Explorar el Código

Improve current lumi UI V4
https://dev.wormwood.com.sg/zentao/task-view-245.html

vbea hace 2 años
padre
commit
1058d098f3
Se han modificado 36 ficheros con 2935 adiciones y 145 borrados
  1. 13 4
      Strides-APP/app/components/BadgeSelectItem.js
  2. 10 3
      Strides-APP/app/components/BottomModal.js
  3. 37 0
      Strides-APP/app/components/ShadowView.js
  4. 5 5
      Strides-APP/app/components/ToolbarUgly.js
  5. 36 0
      Strides-APP/app/components/VbeRadialGradient.js
  6. 2 0
      Strides-APP/app/i18n/locales/en.js
  7. 2 0
      Strides-APP/app/i18n/locales/zh-TW.js
  8. 2 0
      Strides-APP/app/i18n/locales/zh.js
  9. 19 2
      Strides-APP/app/pages/Router.js
  10. 12 6
      Strides-APP/app/pages/alert/ItemAlert.js
  11. 6 7
      Strides-APP/app/pages/alert/ListAlerts.js
  12. 11 5
      Strides-APP/app/pages/alert/ViewAlerts.js
  13. 15 11
      Strides-APP/app/pages/alert/ViewArticle.js
  14. 3 3
      Strides-APP/app/pages/charge/Charging.js
  15. 4 4
      Strides-APP/app/pages/chargeV2/Charging.js
  16. 1 1
      Strides-APP/app/pages/home/DrawerV2.js
  17. 472 0
      Strides-APP/app/pages/home/DrawerV3.js
  18. 5 2
      Strides-APP/app/pages/home/Index.js
  19. 65 23
      Strides-APP/app/pages/home/maps/BottomSiteCard.js
  20. 504 0
      Strides-APP/app/pages/my/ProfileV3.js
  21. 3 3
      Strides-APP/app/pages/search/ConnectType.js
  22. 13 13
      Strides-APP/app/pages/search/ListViewV3.js
  23. 2 2
      Strides-APP/app/pages/search/SearchV3.js
  24. 1 1
      Strides-APP/app/pages/sign/Login.js
  25. 28 23
      Strides-APP/app/pages/sign/ResetPasswordV2.js
  26. 365 0
      Strides-APP/app/pages/signLumi/ForgotPwdVL.js
  27. 316 0
      Strides-APP/app/pages/signLumi/LoginVL.js
  28. 531 0
      Strides-APP/app/pages/signLumi/RegisterVL.js
  29. 220 0
      Strides-APP/app/pages/transaction/HistoryList.js
  30. 196 0
      Strides-APP/app/pages/transaction/PointsHistory.js
  31. 4 5
      Strides-APP/app/pages/transaction/Transaction.js
  32. 7 3
      Strides-APP/app/pages/vouchers/ListPoints.js
  33. 7 3
      Strides-APP/app/pages/vouchers/ListVoucher.js
  34. 6 3
      Strides-APP/app/pages/vouchers/VoucherSelect.js
  35. 1 1
      Strides-APP/ios/Podfile
  36. 11 12
      Strides-APP/package.json

+ 13 - 4
Strides-APP/app/components/BadgeSelectItem.js

@@ -1,6 +1,7 @@
 import React from 'react';
-import { Pressable, Text, View } from 'react-native';
+import { Pressable } from 'react-native';
 import Svg, { Path } from 'react-native-svg';
+import app from '../../app.json';
 
 const BadgeSelectItem = ({
   children,
@@ -25,9 +26,17 @@ const BadgeSelectItem = ({
         <Path
           fill={tintColor}
           d="M28 0H0L40 40V12C40 5.37258 34.6274 -30 28 0Z" />
-        <Path
-          fill="white"
-          d="M34.8965 10.5142L27.1892 18.6408C27.0371 18.801 26.7733 18.7828 26.5999 18.6L26.0834 18.0554L22.63 14.4141C22.4567 14.2313 22.4567 13.935 22.63 13.7522L23.5718 12.7592C23.7451 12.5762 24.0262 12.5762 24.1996 12.7592L26.9138 15.6211L33.3268 8.8593C33.4789 8.69893 33.7426 8.71717 33.916 8.89998L34.8577 9.89294C35.0311 10.0759 35.0484 10.354 34.8965 10.5142Z"/>
+        { app.isLumiWhitelabel
+        ? <></>
+        : <Path
+            fill="white"
+            d="M34.8965 10.5142L27.1892 18.6408C27.0371 18.801 26.7733 18.7828 26.5999 18.6L26.0834 18.0554L22.63 14.4141C22.4567 14.2313 22.4567 13.935 22.63 13.7522L23.5718 12.7592C23.7451 12.5762 24.0262 12.5762 24.1996 12.7592L26.9138 15.6211L33.3268 8.8593C33.4789 8.69893 33.7426 8.71717 33.916 8.89998L34.8577 9.89294C35.0311 10.0759 35.0484 10.354 34.8965 10.5142Z"/>
+        }
+      </Svg>
+    }
+    { (checked && app.isLumiWhitelabel) &&
+      <Svg width={9} height={9} viewBox="0 0 8 8" fill="none" style={{top: 4, right: 3, position: 'absolute'}}>
+        <Path d="M3.89095 0.201172C2.05762 0.201172 0.557617 1.70117 0.557617 3.5345C0.557617 5.36784 2.05762 6.86784 3.89095 6.86784C5.72428 6.86784 7.22428 5.36784 7.22428 3.5345C7.22428 1.70117 5.72428 0.201172 3.89095 0.201172ZM3.89095 6.20117C2.42095 6.20117 1.22428 5.0045 1.22428 3.5345C1.22428 2.06451 2.42095 0.867839 3.89095 0.867839C5.36095 0.867839 6.55762 2.06451 6.55762 3.5345C6.55762 5.0045 5.36095 6.20117 3.89095 6.20117ZM5.42095 2.06117L3.22428 4.25784L2.36095 3.39784L1.89095 3.86784L3.22428 5.20117L5.89095 2.53451L5.42095 2.06117Z" fill="white"/>
       </Svg>
     }
   </Pressable>

+ 10 - 3
Strides-APP/app/components/BottomModal.js

@@ -11,15 +11,22 @@ export const ModalProps = {
   hideModalContentWhileAnimating: true
 }
 
-export default BottomModal = ({visible=false, onHide, children}) => {
+export default BottomModal = ({
+  visible=false,
+  onHide,
+  children,
+  backdropOpacity=0.7,
+  style=styles.bottomModalView,
+  contentStyle=styles.bottomModalContent}) => {
   return (
     <Modal
       isVisible={visible}
       onBackButtonPress={onHide}
       onBackdropPress={onHide}
       useNativeDriver={!isIOS}
-      style={styles.bottomModalView}>
-      <View style={styles.bottomModalContent}>
+      backdropOpacity={backdropOpacity}
+      style={style}>
+      <View style={contentStyle}>
         {children}
       </View>
     </Modal>

+ 37 - 0
Strides-APP/app/components/ShadowView.js

@@ -0,0 +1,37 @@
+import React from 'react';
+import { StyleSheet, Text, View } from 'react-native';
+import Svg, { Defs, Ellipse, G, LinearGradient, Path, Rect, Stop } from 'react-native-svg';
+
+/**
+ * 阴影组件
+ * @param {offset} 阴影左右偏移量(配合style实现margin)
+ * @returns React Element
+ */
+const ShadowView = ({
+  offset=32,
+  style=styles.shadowView,
+  shadowColor="#888888"
+}) => (
+  <View style={style}>
+    <Svg width={$vw(100)-offset} height={$vw(4.36)} viewBox="0 0 342 15" fill="none">
+      <Path d="M0 0H342V7C342 11.4183 338.418 15 334 15H8.00001C3.58173 15 0 11.4183 0 7V0Z" fill="url(#paint0_linear_4117_1875)"/>
+      <Defs>
+        <LinearGradient id="paint0_linear_4117_1875" x1="171" y1="15" x2="171" y2="0" gradientUnits="userSpaceOnUse">
+          <Stop stopColor="#F9F9F9"/>
+          <Stop offset="1" stopColor={shadowColor}/>
+        </LinearGradient>
+      </Defs>
+    </Svg>
+  </View>
+);
+
+export default ShadowView;
+
+const styles = StyleSheet.create({
+  shadowView: {
+    opacity: 0.8,
+    marginTop: -6,
+    marginLeft: 16,
+    marginRight: 16
+  }
+})

+ 5 - 5
Strides-APP/app/components/ToolbarUgly.js

@@ -44,19 +44,19 @@ const styles = StyleSheet.create({
     backgroundColor: colorLight
   },
   uglyIcon: {
-    height: 44,
+    height: 40,
     paddingLeft: 16,
-    paddingRight: 16,
+    paddingRight: 8,
     alignItems: 'center',
     justifyContent: 'center'
   },
   titleText: {
     flex: 1,
     color: textPrimary,
-    padding: 8,
+    padding: 0,
     fontSize: 20,
     fontWeight: 'bold',
-    textAlign: 'center',
-    textTransform: 'uppercase'
+    textAlign: 'left',
+    //textTransform: 'uppercase'
   }
 })

+ 36 - 0
Strides-APP/app/components/VbeRadialGradient.js

@@ -0,0 +1,36 @@
+import React from 'react';
+import Svg, {
+  Defs,
+  RadialGradient,
+  Rect,
+  Stop,
+} from 'react-native-svg';
+
+const VbeRadialGradient = ({ colorList=[], x, y, rx, ry }) => {
+  return (
+    <Svg height="100%" width="100%">
+      <Defs>
+        <RadialGradient
+          id="grad"
+          cx={x}
+          cy={y}
+          rx={rx}
+          ry={ry}
+          gradientUnits="userSpaceOnUse"
+        >
+          { colorList.map((value, index) => (
+            <Stop
+              key={`RadialGradientItem_${index}`}
+              offset={value.offset}
+              stopColor={value.color}
+              stopOpacity={value.opacity}
+            />
+          ))}
+        </RadialGradient>
+      </Defs>
+      <Rect x="0" y="0" width="100%" height="100%" fill="url(#grad)" />
+    </Svg>
+  );
+};
+
+export default VbeRadialGradient;

+ 2 - 0
Strides-APP/app/i18n/locales/en.js

@@ -90,7 +90,9 @@ export default {
     back2Login: "Back to Login",
     btnLogin: "LOGIN",
     btnSendOTP: "EMAIL OTP",
+    btnSignIn: "SIGN IN",
     btnSignUp: "SIGN UP",
+    btnRegister: "REGISTER",
     errContactNoFormat: "Phone Number is incorrect format",
     errEmailFormat: "Email is incorrect format",
     errPasswordConfirm: "The twice passwords are inconsistent",

+ 2 - 0
Strides-APP/app/i18n/locales/zh-TW.js

@@ -90,7 +90,9 @@ export default {
     back2Login: "去登入",
     btnLogin: "登入",
     btnSendOTP: "獲取校驗碼",
+    btnSignIn: "登入",
     btnSignUp: "注冊",
+    btnRegister: "注冊",
     errContactNoFormat: "電話號碼格式不正確",
     errEmailFormat: "信箱格式不正確",
     errPasswordConfirm: "兩此輸入密碼不壹致",

+ 2 - 0
Strides-APP/app/i18n/locales/zh.js

@@ -90,7 +90,9 @@ export default {
     back2Login: "回到登录",
     btnLogin: "登录",
     btnSendOTP: "获取验证码",
+    btnSignIn: "登录",
     btnSignUp: "注册",
+    btnRegister: "注册",
     errContactNoFormat: "电话号码格式不正确",
     errEmailFormat: "邮箱格式不正确",
     errPasswordConfirm: "两此输入密码不一致",

+ 19 - 2
Strides-APP/app/pages/Router.js

@@ -25,6 +25,7 @@ import QRScan from './charge/QRScan';
 import Feedback from './my/Feedback';
 import Privacy from './my/Privacy';
 import Profile from './my/ProfileV2';
+import ProfileV3 from './my/ProfileV3';
 import Condition from './my/Condition';
 import Summary from './chargeV2/SummaryV2';
 import SummaryV3 from './chargeV2/SummaryV3';
@@ -71,7 +72,10 @@ import VoucherPage from './vouchers/VoucherPage';
 import VoucherSelect from './vouchers/VoucherSelect';
 import VoucherDetails from './vouchers/VoucherDetails';
 import PointsHistory from './vouchers/PointsHistory';
-import Transaction from './wallet/Transaction';
+import Transaction from './transaction/Transaction';
+import LoginVL from './signLumi/LoginVL';
+import RegisterVL from './signLumi/RegisterVL';
+import ForgotPwdVL from './signLumi/ForgotPwdVL';
 
 export var PageList = {
   'splash': {
@@ -90,13 +94,18 @@ export var PageList = {
     component: app.isLumiWhitelabel ? SearchV3 : Search
   },
   'login': {
-    component: Login
+    component: app.isLumiWhitelabel ? LoginVL : Login
   },
   'register': {
     component: RegisterV4,
     title: 'Public Registration',
     titleScope: 'route.publicRegister'
   },
+  'registerLumi': {
+    component: RegisterVL,
+    title: 'Registration',
+    titleScope: 'sign.btnRegister'
+  },
   'registerPublic': {
     component: RegisterPublic,
     title: 'Public Registration',
@@ -162,6 +171,9 @@ export var PageList = {
     titleScope: 'route.profileSettings',
     component: Profile
   },
+  'profileV3': {
+    component: ProfileV3
+  },
   'summary': {
     title: 'Summary',
     titleScope: 'receipt.receipt',
@@ -330,6 +342,11 @@ export var PageList = {
     titleScope: 'route.forgotPassword',
     component: ResetPassword
   },
+  'forgotPasswordLumi': {
+    title: 'Forgot Password',
+    titleScope: 'route.forgotPassword',
+    component: ForgotPwdVL
+  },
   'changePassword': {
     title: 'Account Security',
     titleScope: 'route.changePassword',

+ 12 - 6
Strides-APP/app/pages/alert/ItemAlert.js

@@ -97,15 +97,15 @@ export default ItemAlert = ({item, index, separators, onPress, onLongPress}) =>
   }
   return (
     <Pressable
-      style={item.readStatus ? styles.notyItemView : styles.unotyItemView}
+      style={styles.notyItemView}
       onPress={onPress}
       android_ripple={ripple}
       onLongPress={onLongPress}>
-      <IconType
+      {/* <IconType
         style={styles.iconType}
         type={item.notificationType}
         color={getDotColor()}
-      />
+      /> */}
       <View style={styles.itemContent}>
         <TextView
           style={styles.textTitle}
@@ -124,10 +124,16 @@ export default ItemAlert = ({item, index, separators, onPress, onLongPress}) =>
         </TextView>
         <TextView
           style={styles.textMessage}
-          numberOfLines={2}>
+          numberOfLines={1}>
           {item.notificationText}
         </TextView>
       </View>
+      { !item.readStatus &&
+        <MaterialIcons
+          name="circle"
+          size={16}
+          color={"#ED3F3F"}/>
+      }
     </Pressable>
   )
 }
@@ -158,7 +164,7 @@ const styles = StyleSheet.create({
   textTitle: {
     flex: 1,
     color: textPrimary,
-    fontSize: 15,
+    fontSize: 14,
     fontWeight: 'bold',
     paddingBottom: 4
   },
@@ -173,6 +179,6 @@ const styles = StyleSheet.create({
   },
   textMessage: {
     color: textPrimary,
-    fontSize: 12
+    fontSize: 10
   }
 })

+ 6 - 7
Strides-APP/app/pages/alert/ListAlerts.js

@@ -228,18 +228,17 @@ const styles = StyleSheet.create({
   },
   topButton: {
     marginRight: 4,
+    borderRadius: 4,
     flexDirection: 'row',
-    backgroundColor: 'white',
-    borderColor: textCancel,
-    borderWidth: 1,
-    borderStyle: 'solid'
+    backgroundColor: colorPrimary
   },
   topButtonView: {
-    ...$padding(6, 16),
+    ...$padding(5, 8),
     alignItems: 'center'
   },
   topButtonText: {
-    color: textPrimary,
-    fontSize: 12
+    color: textLight,
+    fontSize: 12,
+    textTransform: "uppercase"
   }
 })

+ 11 - 5
Strides-APP/app/pages/alert/ViewAlerts.js

@@ -82,8 +82,8 @@ export default class ViewAlerts extends Component {
               {this.state.messageInfo.createTime}
             </TextView>
           </View>
-          
         </View>
+        <View style={styles.divideView}></View>
         <ScrollView
           style={ui.flex1}>
           <TextView
@@ -112,23 +112,29 @@ const styles = StyleSheet.create({
   },
   textTitle: {
     color: textPrimary,
-    fontSize: 18,
+    fontSize: 14,
     fontWeight: 'bold',
     paddingBottom: 2
   },
   textDate: {
     color: textSecondary,
-    fontSize: 10,
+    fontSize: 12,
     paddingLeft: 2
   },
   header: {
     padding: 16,
-    ...ElevationObject(1),
+    //...ElevationObject(1),
     backgroundColor: pageBackground
   },
+  divideView: {
+    height: 1,
+    marginLeft: 16,
+    marginRight: 16,
+    backgroundColor: colorPrimary
+  },
   textMessage: {
     color: textPrimary,
-    fontSize: 14,
+    fontSize: 12,
     padding: 16
   }
 })

+ 15 - 11
Strides-APP/app/pages/alert/ViewArticle.js

@@ -102,6 +102,8 @@ export default class ViewArticle extends Component {
               numberOfLines={1}>
               {this.state.messageInfo.createTime}
             </TextView>
+          </View>
+          <View style={ui.flexc}>
             <MaterialCommunityIcons
               name="eye-check-outline"
               size={12}
@@ -151,33 +153,35 @@ const styles = StyleSheet.create({
   },
   textTitle: {
     color: textPrimary,
-    fontSize: 18,
+    fontSize: 14,
     fontWeight: 'bold',
     paddingBottom: 2
   },
   textDate: {
     flex: 1,
     color: textSecondary,
-    fontSize: 10,
+    fontSize: 12,
     paddingLeft: 2
   },
   textView: {
     color: textSecondary,
-    fontSize: 10,
-    paddingLeft: 4
+    fontSize: 14,
+    padding: 4
   },
   header: {
     padding: 16,
-    ...ElevationObject(1),
+    //...ElevationObject(1),
     backgroundColor: pageBackground
   },
   textMessage: {
     color: textPrimary,
-    fontSize: 14,
-    padding: 16
+    fontSize: 12,
+    paddingLeft: 16,
+    paddingRight: 16
   },
   labelTypeText: {
-    fontSize: 11,
+    fontSize: 12,
+    fontWeight: 'bold',
     borderWidth: 1,
     borderRadius: 4,
     borderColor: colorPrimary,
@@ -186,7 +190,7 @@ const styles = StyleSheet.create({
   },
   textLinkTitle: {
     color: textPrimary,
-    fontSize: 16,
+    fontSize: 14,
     fontWeight: 'bold',
     padding: 16
   },
@@ -195,12 +199,12 @@ const styles = StyleSheet.create({
     flexDirection: 'row'
   },
   linkIndex: {
-    fontSize: 14,
+    fontSize: 12,
     paddingRight: 2
   },
   linkHyper: {
     ...ui.link,
-    fontSize: 14,
+    fontSize: 12,
     textDecorationLine: 'underline'
   },
   linkActive: {

+ 3 - 3
Strides-APP/app/pages/charge/Charging.js

@@ -4,7 +4,7 @@
  */
 import React, { useRef, useEffect, useState } from 'react';
 import { Animated, View, Easing, StyleSheet, Image, Text, ImageBackground, TextInput } from 'react-native';
-import { RadialGradient } from 'react-native-gradients';
+import VbeRadialGradient from '../../components/VbeRadialGradient';
 import Modal from 'react-native-modal';
 import { ModalProps } from '../../components/BottomModal';
 import ChargeItemSelect from '../../icons/ChargeItemSelect';
@@ -86,13 +86,13 @@ export const CircleAnimate = ({isStart = false}) => {
       ]}>
       { isStart &&
         <View style={{ width: 25, height: 25, marginTop: -11}}>
-          <RadialGradient
+          <VbeRadialGradient
             x="50%" y="50%" rx="50%" ry="50%"
             colorList={[
               {offset: '0%', color: '#FFF', opacity: .8},
               {offset: '40%', color: '#FFF', opacity: .5},
               {offset: '70%', color: '#FFF', opacity: .3},
-              {offset: '100%', color: '#FFF',opacity: 0}
+              {offset: '100%', color: '#FFF', opacity: 0}
             ]}/>
         </View>
       }

+ 4 - 4
Strides-APP/app/pages/chargeV2/Charging.js

@@ -3,8 +3,8 @@
  * @邠心vbe on 2021/04/13
  */
 import React, { useRef, useEffect, useState } from 'react';
-import { Animated, View, Easing, StyleSheet, Image, Text, ImageBackground, TextInput } from 'react-native';
-import { RadialGradient } from 'react-native-gradients';
+import { Animated, View, Easing, StyleSheet, Image, ImageBackground, TextInput } from 'react-native';
+import VbeRadialGradient from '../../components/VbeRadialGradient';
 import Modal from 'react-native-modal';
 import { ModalProps } from '../../components/BottomModal';
 import Button, { ElevationObject, ViewHeight } from '../../components/Button';
@@ -87,13 +87,13 @@ export const CircleAnimate = ({isStart = false}) => {
       ]}>
       { isStart &&
         <View style={{ width: 25, height: 25, marginTop: -11}}>
-          <RadialGradient
+          <VbeRadialGradient
             x="50%" y="50%" rx="50%" ry="50%"
             colorList={[
               {offset: '0%', color: '#FFF', opacity: .8},
               {offset: '40%', color: '#FFF', opacity: .5},
               {offset: '70%', color: '#FFF', opacity: .3},
-              {offset: '100%', color: '#FFF',opacity: 0}
+              {offset: '100%', color: '#FFF', opacity: 0}
             ]}/>
         </View>
       }

+ 1 - 1
Strides-APP/app/pages/home/DrawerV2.js

@@ -128,7 +128,7 @@ export default DrawerV2 = ({isLogin=false, userInfo, onLogout, sideCountInfo={},
         <Button
           style={styles.itemButton}
           viewStyle={styles.itemView}
-          onClick={() => startPage(PageList.profile)}>
+          onClick={() => startPage(app.isLumiWhitelabel ? PageList.profileV3 : PageList.profile)}>
           <MaterialCommunityIcons
             style={styles.icon}
             name="account"

+ 472 - 0
Strides-APP/app/pages/home/DrawerV3.js

@@ -0,0 +1,472 @@
+/**
+ * 新版首页抽屉菜单
+ * @邠心vbe on 2024/02/01
+ */
+import React from 'react';
+import { Path, Svg } from 'react-native-svg';
+import { Image, StyleSheet, Text, View } from 'react-native';
+import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
+import app from '../../../app.json';
+import { PageList } from '../Router';
+import utils from '../../utils/utils';
+import apiCharge from '../../api/apiCharge';
+import Button from '../../components/Button';
+import Dialog from '../../components/Dialog';
+import TextView from '../../components/TextView';
+import { BackButton } from '../../components/Toolbar';
+import { toTopupPage } from '../payment/PaymentConfig';
+
+const DEBUG = app.debug && !app.product;
+
+export default DrawerV3 = ({isLogin=false, userInfo, onLogout, sideCountInfo={}, navigation}) => {
+  const getCharging = () => {
+    Dialog.showProgressDialog();
+    apiCharge.getUserCharging().then(res => {
+      Dialog.dismissLoading();
+      if (res.data.sitePk) {
+        utils.toChargeDetailPage(res.data.sitePk, 'view', PageList.home);
+        //startPage(PageList.chargeDetailPage, {stationInfo: {id: res.data.sitePk}, action: 'view', from: PageList.home});
+        //startPage(PageList.chargeDetail, { stationInfo: {id: res.data.sitePk}, action: 'view'});
+      } else if (res.msg) {
+        toastShort(res.msg);
+      } else {
+        toastShort($t("drawer.noChargingSession"));
+      }
+    }).catch((err) => {
+      if (app.debug)
+        console.log(err);
+      Dialog.dismissLoading();
+      toastShort($t("drawer.noChargingSession"));
+    })
+  }
+
+  const logout = () => {
+    Dialog.showDialog({
+      title: $t('profile.signOut'),
+      message: $t('profile.tipSignOut'),
+      callback: btn => {
+        if (btn == Dialog.BUTTON_OK) {
+          Dialog.showProgressDialog();
+          setTimeout(() => {
+            if (onLogout) onLogout();
+          }, 500);
+        }
+      }
+    })
+  }
+
+  return (
+    <View style={styles.drawerView}>
+      <View style={styles.loginView}>
+        {/* <View style={ui.flexcw}>
+          { (isLogin && userInfo.photoUrl)
+          ? <TouchableWithoutFeedback>
+              <Image
+                style={styles.avatar}
+                source={{uri: utils.getImageUrl(userInfo.photoUrl)}}/>
+            </TouchableWithoutFeedback>
+          : <TouchableWithoutFeedback>
+              <Image
+                style={styles.avatar}
+                source={require('../../images/user/ic-avatar-default.png')}/>
+            </TouchableWithoutFeedback>
+          }
+          <BackButton
+            style={styles.closeMenu}
+            onPress={() => navigation?.toggleDrawer()}>
+            <MaterialIcons
+              name="menu-open"
+              size={24}
+              color={colorLight}/>
+          </BackButton>
+        </View> */}
+        { isLogin
+        ? <View
+            style={styles.nickViewStyle}>
+            <View style={ui.flex1}>
+              <TextView
+                style={styles.nickname}
+                ellipsizeMode='tail'
+                numberOfLines={1}>
+                { userInfo.nickName 
+                ? userInfo.nickName
+                : $t('drawer.logging')
+                }
+              </TextView>
+              <TextView
+                style={styles.emailText}
+                ellipsizeMode='tail'
+                numberOfLines={1}>{userInfo.email}</TextView>
+            </View>
+            <BackButton
+              onPress={logout}>
+              <MaterialIcons
+                size={26}
+                color={textPrimary}
+                name='exit-to-app'/>
+            </BackButton>
+          </View>
+        : <Button
+            style={styles.nickView}
+            viewStyle={styles.nickViewStyle}
+            onClick={() => startPage(PageList.login)}>
+            <TextView
+              style={styles.loginText}
+              ellipsizeMode='tail'
+              numberOfLines={1}>
+              {$t('drawer.sign')}
+            </TextView>
+            <FontAwesome
+              size={24}
+              color={textPrimary}
+              name='angle-right'/>
+          </Button>
+        }
+      </View>
+      <View style={styles.divideLogin}></View>
+      { isLogin && <>
+        <Button
+          style={styles.itemButton}
+          viewStyle={styles.itemView}
+          onClick={() => startPage(app.isLumiWhitelabel ? PageList.profileV3 : PageList.profile)}>
+          <MaterialCommunityIcons
+            style={styles.icon}
+            name="account-circle-outline"
+            color={textPrimary}
+            size={24}/>
+          <TextView style={styles.label}>{$t('route.profileSettings')}</TextView>
+        </Button>
+        <Button
+          style={styles.itemButton}
+          viewStyle={styles.itemView}
+          onClick={() => getCharging()}>
+          <MaterialCommunityIcons
+            style={styles.icon}
+            name="gas-station-outline"
+            color={textPrimary}
+            size={24}/>
+          <TextView style={styles.label}>{$t('drawer.charging')}</TextView>
+        </Button>
+        <Button
+          style={styles.itemButton}
+          viewStyle={styles.itemView}
+          onClick={() => {
+            startPage(app.isLumiWhitelabel ? PageList.transaction : PageList.wallet);
+          }}>
+          <MaterialCommunityIcons
+            style={styles.icon}
+            name="finance"
+            color={textPrimary}
+            size={24}/>
+          <TextView style={styles.label}>{$t('drawer.wallet')}</TextView>
+        </Button>
+        <Button
+          style={styles.itemButton}
+          viewStyle={styles.itemView}
+          onClick={() => toTopupPage()}>
+          <MaterialCommunityIcons
+            style={styles.icon}
+            name="wallet-plus-outline"
+            color={textPrimary}
+            size={24}/>
+          <TextView style={styles.label}>{'Wallet'}</TextView>
+          <Text style={styles.balanceText2}>{userInfo.creditStr}</Text>
+        </Button>
+        </>
+      }
+      { (app.v3.vouchers && isLogin) &&
+        <Button
+          style={styles.itemButton}
+          viewStyle={styles.itemView}
+          onClick={() => startPage(PageList.myVoucher)}>
+          <MaterialCommunityIcons
+            style={styles.icon}
+            name="ticket-percent-outline"
+            color={textPrimary}
+            size={24}/>
+          <TextView style={styles.label}>{$t('voucher.vouchers')}</TextView>
+          <TextView
+            style={styles.balanceText}
+            fixedAlign={false}>
+            <Text style={styles.balanceText2}>{sideCountInfo?.activeVoucherCount || 0}</Text>  Active
+          </TextView>
+        </Button>
+      }
+      { (app.notifications.enable && isLogin) &&
+        <Button
+          style={styles.itemButton}
+          viewStyle={styles.itemView}
+          onClick={() => {
+            startPage(PageList.notification);
+          }}>
+          <MaterialCommunityIcons
+            style={styles.icon}
+            name="bell-badge-outline"
+            color={textPrimary}
+            size={24}/>
+          <TextView style={styles.label}>{$t('route.notifications')}</TextView>
+          <TextView
+            style={styles.balanceText}
+            fixedAlign={false}>
+            <Text style={styles.balanceText2}>{sideCountInfo?.toBeReadCount || 0}</Text>  Unread
+          </TextView>
+        </Button>
+      }
+      { (app.modules.bookmarks && isLogin) &&
+        <Button
+          style={styles.itemButton}
+          viewStyle={styles.itemView}
+          onClick={() => {
+            startPage(PageList.bookmarks);
+          }}>
+          <MaterialCommunityIcons
+            style={styles.icon}
+            name="bookmark-check-outline"
+            color={textPrimary}
+            size={24}/>
+          <TextView style={styles.label}>{$t('route.bookmarks')}</TextView>
+          <TextView
+            style={styles.balanceText}
+            fixedAlign={false}>
+            <Text style={styles.balanceText2}>{sideCountInfo?.bookMarkCount || 0}</Text>  Saved
+          </TextView>
+        </Button>
+      }
+      {/*附加功能-结束*/}
+      { (app.modules.membership && isLogin) &&
+        <Button
+          style={styles.itemButton}
+          viewStyle={styles.itemView}
+          onClick={() => {
+            startPage(PageList.membersList);
+          }}>
+          <Svg 
+            style={styles.icon}
+            width={24}
+            height={24}
+            viewBox='0 0 24 24'>
+            <Path
+              fill={textPrimary}
+              d="M7 4C4.8 4 3 5.8 3 8C3 10.2 4.8 12 7 12C9.2 12 11 10.2 11 8C11 5.8 9.2 4 7 4ZM7 10C5.9 10 5 9.1 5 8C5 6.9 5.9 6 7 6C8.1 6 9 6.9 9 8C9 9.1 8.1 10 7 10ZM0 18C0 15.8 3.1 14 7 14C8.5 14 9.9 14.3 11 14.7V17C10.2 16.5 8.8 16 7 16C3.8 16 2 17.4 2 18H11V20H0V18ZM22 4H15C13.9 4 13 4.9 13 6V18C13 19.1 13.9 20 15 20H22C23.1 20 24 19.1 24 18V6C24 4.9 23.1 4 22 4ZM22 18H15V6H22V18Z"/>
+          </Svg>
+          <TextView style={styles.label}>{$t('drawer.members')}</TextView>
+          <TextView
+            style={styles.balanceText}
+            fixedAlign={false}>
+            <Text style={styles.balanceText2}>{sideCountInfo?.membershipCount || 0}</Text>  Active
+          </TextView>
+        </Button>
+      }
+      {/* <Button
+        style={styles.itemButton}
+        viewStyle={styles.itemView}
+        onClick={() => {
+          startPage(PageList.feedback);
+        }}>
+        <MaterialCommunityIcons
+          style={styles.icon}
+          name="message-alert-outline"
+          color="#222"
+          size={24}/>
+        <TextView style={styles.label}>{$t('drawer.feedback')}</TextView>
+      </Button> */}
+      {/* <Button
+        style={styles.itemButton}
+        viewStyle={styles.itemView}
+        onClick={() => {
+          startPage(PageList.settings);
+        }}>
+        <MaterialIcons
+          style={styles.icon}
+          name="settings"
+          color="#222"
+          size={25}
+        />
+        <TextView style={styles.label}>{$t('drawer.settings')}</TextView>
+      </Button> */}
+      { app.modules.support &&
+        <Button
+          style={styles.itemButton}
+          viewStyle={styles.itemView}
+          onClick={() => {
+            startPage(PageList.supportContact);
+          }}>
+          <MaterialCommunityIcons
+            style={styles.icon}
+            name="face-agent"
+            color="#333"
+            size={26}
+          />
+          <TextView style={styles.label}>{$t('drawer.contactSupport')}</TextView>
+        </Button>
+      }
+      { DEBUG && <>
+        <Button
+          style={styles.itemButton}
+          viewStyle={styles.itemView}
+          onClick={() => {
+            startPage(PageList.notify);
+          }}>
+          <MaterialCommunityIcons
+            style={styles.icon}
+            name="dev-to"
+            color="#333"
+            size={26}
+          />
+          <TextView style={styles.label}>{$t('route.notificationTest')}</TextView>
+        </Button>
+        <Button
+          style={styles.itemButton}
+          viewStyle={styles.itemView}
+          onClick={() => {
+            startPage(PageList.selectVoucher, {chargeBoxId: "LUMI-TEST", connectorId: 1});
+          }}>
+          <MaterialCommunityIcons
+            style={styles.icon}
+            name="dev-to"
+            color="#333"
+            size={26}
+          />
+          <TextView style={styles.label}>{"Test Page"}</TextView>
+        </Button>
+      </> }
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  drawerView: {
+    paddingTop: 8,
+    paddingBottom: 8,
+  },
+  loginView: {
+    paddingTop: 0,
+    paddingBottom: 0,
+    backgroundColor: colorLight
+  },
+  avatar: {
+    width: 60,
+    height: 60,
+    marginLeft: 24,
+    borderWidth: 2,
+    borderRadius: 80,
+    borderColor: colorPrimary,
+  },
+  closeMenu: {
+    width: 40,
+    height: 40,
+    marginRight: 16,
+    borderRadius: 40,
+    alignItems: 'center',
+    justifyContent: 'center',
+    backgroundColor: colorPrimary
+  },
+  nickView: {
+    marginTop: 2,
+    borderRadius: 0,
+    backgroundColor: colorLight
+  },
+  nickViewStyle: {
+    flex: 1,
+    alignItems: 'center',
+    flexDirection: 'row',
+    ...$padding(0, 4, 10)
+  },
+  nickname: {
+    flex: 1,
+    color: textPrimary,
+    fontSize: 20,
+    fontWeight: 'bold',
+    paddingLeft: 16,
+  },
+  emailText: {
+    color: textPrimary,
+    fontSize: 10,
+    marginTop: -5,
+    paddingLeft: 16,
+    paddingBottom: 4
+  },
+  loginText: {
+    flex: 1,
+    color: textPrimary,
+    fontSize: 18,
+    fontWeight: 'bold',
+    marginTop: 4,
+    paddingTop: 4,
+    paddingLeft: 16,
+    paddingBottom: 4,
+    textTransform: 'uppercase'
+  },
+  divideLogin: {
+    height: 8,
+    marginTop: 2,
+    marginRight: 0,
+    marginBottom: 8,
+    backgroundColor: colorPrimary
+  },
+  itemButton: {
+    borderRadius: 0,
+    backgroundColor: colorLight
+  },
+  itemView: {
+    flex: 1,
+    height: 48,
+    paddingLeft: 16,
+    marginBottom: 0,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  icon: {
+    width: 24,
+    height: 24,
+    marginRight: 16
+  },
+  label: {
+    flex: 1,
+    color: textPrimary,
+    fontSize: 14
+  },
+  divided: {
+    height: 1,
+    marginTop: 24,
+    marginLeft: 16,
+    backgroundColor: colorAccent
+  },
+  balanceText: {
+    color: textPrimary,
+    fontSize: 14,
+    marginRight: 20
+  },
+  balanceText2: {
+    color: colorPrimary,
+    fontSize: 14,
+    fontWeight: 'bold',
+    marginRight: 20
+  },
+  bridgeText: {
+    width: 20,
+    height: 20,
+    color: textLight,
+    fontSize: 12,
+    marginRight: 16,
+    borderRadius: 30,
+    fontWeight: 'bold',
+    alignItems: 'center',
+    flexDirection: 'row',
+    justifyContent: 'center',
+    backgroundColor: "#FF3B30"
+  },
+  bridgeText2: {
+    width: 22,
+    height: 22,
+    color: textLight,
+    fontSize: 10,
+    marginRight: 16,
+    borderRadius: 30,
+    fontWeight: 'bold',
+    alignItems: 'center',
+    flexDirection: 'row',
+    justifyContent: 'center',
+    backgroundColor: "#FF3B30"
+  }
+});

+ 5 - 2
Strides-APP/app/pages/home/Index.js

@@ -15,6 +15,7 @@ import utils from '../../utils/utils';
 import apiNotification from '../../api/apiNotification';
 import DrawerView from './Drawer.js';
 import DrawerViewV2 from './DrawerV2.js';
+import DrawerViewV3 from './DrawerV3.js';
 
 const Drawer = createDrawerNavigator();
 
@@ -134,7 +135,7 @@ export default class Home extends Component {
           headerShown: false,
           drawerType: global.$width >= 768 ? 'back' : 'front',
           drawerStyle: {
-            width: app.v3.drawer ? $vw(100) : ($vw(75) > 320 ? 320 : $vw(75)),
+            width: app.v3.drawer && !app.isLumiWhitelabel ? $vw(100) : ($vw(75) > 320 ? 320 : $vw(75)),
             backgroundColor: app.v3.drawer ? 'rgba(0,0,0,0.1)' : colorLight
           },
           swipeEnabled: !app.v3.drawer, //启用侧滑打开抽屉
@@ -152,7 +153,9 @@ const CustomerDrawerContent = (props) => {
       canCancelContentTouches={true}
       style={app.v3.drawer ? styles.contentV2 : {}}>
       { app.v3.drawer
-      ? <DrawerViewV2 {...props}/>
+      ? app.isLumiWhitelabel
+        ? <DrawerViewV3 {...props}/>
+        : <DrawerViewV2 {...props}/>
       : <DrawerView {...props}/>
       }
     </DrawerContentScrollView>

+ 65 - 23
Strides-APP/app/pages/home/maps/BottomSiteCard.js

@@ -76,12 +76,29 @@ export const BottomSiteCard = ({
       visible={visible}
       onHide={() => {
         onClose()
-      }}>
+      }}
+      backdropOpacity={0}
+      contentStyle={styles.bottomModal}>
       <View style={styles.stationBarView}>
+        <View style={ui.flexc}>
+          <TextView
+            ellipsizeMode='tail'
+            numberOfLines={1}
+            style={styles.stationTitle}>{stationInfo.name}</TextView>
+          <Pressable
+            style={styles.closeIcon}
+            android_ripple={rippleLess}
+            onPress={onClose}>
+            <MaterialCommunityIcons
+              name="close"
+              size={22}
+              color={textSecondary}/>
+          </Pressable>
+        </View>
         <TextView
+          style={styles.stationAddress}
           ellipsizeMode='tail'
-          numberOfLines={1}
-          style={styles.stationTitle}>{stationInfo.name}</TextView>
+          numberOfLines={3}>{stationInfo.address}</TextView>
         <View style={ui.flexc}>
           <TextView style={styles.siteTypes}>{stationInfo.siteType}</TextView>
           { (stationInfo.allConnector && stationInfo.allConnector.available > 0) &&
@@ -97,10 +114,29 @@ export const BottomSiteCard = ({
           <ConnectTypeV2 {...stationInfo?.acConnector}/>
           <ConnectTypeV2 {...stationInfo?.dcConnector}/>
         </View>
-        <TextView
-          style={styles.stationAddress}
-          ellipsizeMode='tail'
-          numberOfLines={3}>{stationInfo.address}</TextView>
+        <View style={styles.infoDetailsView}>
+          <View style={ui.flex1}>
+            <TextView style={styles.infoTitle}>{$t("charging.operatingHours")}</TextView>
+            <View style={styles.infoView}>
+              <TextView style={styles.infoText}>{getOperatingHours()}</TextView>
+            </View>
+          </View>
+          <View style={{width: 4}}></View>
+          <View style={ui.flex1}>
+            <TextView style={styles.infoTitle}>{$t("charging.parkingFees")}</TextView>
+            <View style={styles.infoView}>
+              <TextView style={styles.infoText}>{getParkingFee()}</TextView>
+            </View>
+          </View>
+          <View style={{width: 4}}></View>
+          <View style={ui.flex1}>
+            <TextView style={styles.infoTitle}>{$t("charging.additionalInfo")}</TextView>
+            <View style={styles.infoView}>
+              <TextView style={styles.infoText}>{stationInfo?.additionalNotes}</TextView>
+            </View>
+          </View>
+        </View>
+        <EndView half/>
         <View style={ui.flexc}>
           { stationInfo.upcoming
           ? <View style={[ui.center, $margin(0, 8)]}>
@@ -119,7 +155,7 @@ export const BottomSiteCard = ({
               }}>
               <MaterialCommunityIcons
                 name="directions"
-                size={18}
+                size={20}
                 color={textPrimary}/>
               <TextView style={styles.directText}>Directions</TextView>
             </Pressable>
@@ -129,7 +165,7 @@ export const BottomSiteCard = ({
                 onPress={onFavorite}>
                 <MaterialIcons
                   name="star"
-                  size={18}
+                  size={20}
                   color={stationInfo.favorite ? textLight : textPrimary}/>
                 <TextView style={[styles.directText, stationInfo.favorite ? styles.bookmarked : {}]}>
                   {stationInfo.favorite ? "Saved" : "Save"}
@@ -153,7 +189,7 @@ export const BottomSiteCard = ({
               onPress={() => toChargePage()}>
               <MaterialCommunityIcons
                 name="information-variant"
-                size={17}
+                size={20}
                 color={textPrimary}/>
               <TextView style={styles.directText}>More Info</TextView>
             </Pressable>
@@ -175,6 +211,9 @@ export const BottomSiteCard = ({
 }
 
 const styles = StyleSheet.create({
+  bottomModal: {
+    backgroundColor: pageBackground
+  },
   stationBarView: {
     padding: 16
   },
@@ -190,6 +229,7 @@ const styles = StyleSheet.create({
     justifyContent: 'space-around'
   },
   stationTitle: {
+    flex: 1,
     color: textPrimary,
     fontSize: 24,
     fontWeight: 'bold',
@@ -197,14 +237,14 @@ const styles = StyleSheet.create({
   },
   stationAddress: {
     color: textSecondary,
-    fontSize: 12,
-    paddingTop: 4,
-    paddingBottom: 6
+    fontSize: 14,
+    paddingTop: 6,
+    paddingBottom: 8
   },
   siteTypes: {
     color: textLight,
     height: 20,
-    fontSize: 8,
+    fontSize: 12,
     marginRight: 6,
     borderRadius: 30,
     ...$padding(0, 10),
@@ -213,7 +253,7 @@ const styles = StyleSheet.create({
   stationAvailable: {
     color: textPrimary,
     height: 20,
-    fontSize: 8,
+    fontSize: 12,
     marginRight: 6,
     borderRadius: 30,
     borderWidth: 1,
@@ -234,6 +274,7 @@ const styles = StyleSheet.create({
   },
   directIconView: {
     zIndex: 1,
+    height: 32,
     marginRight: 10,
     borderWidth: 1,
     borderRadius: 30,
@@ -241,7 +282,7 @@ const styles = StyleSheet.create({
     alignItems: 'center',
     flexDirection: 'row',
     justifyContent: 'center',
-    ...$padding(2, 8, 2, 4)
+    ...$padding(2, 10, 2, 6)
   },
   bookmarked: {
     color: textLight,
@@ -250,7 +291,7 @@ const styles = StyleSheet.create({
   },
   directText: {
     color: textPrimary,
-    fontSize: 12,
+    fontSize: 14,
     paddingLeft: 4
   },
   connectView: {
@@ -265,25 +306,26 @@ const styles = StyleSheet.create({
     backgroundColor: '#AEAEAE'
   },
   infoDetailsView: {
+    paddingTop: 8,
     flexDirection: 'row'
   },
   infoTitle: {
-    color: '#000',
-    fontSize: 12,
-    fontWeight: 'bold',
+    color: textPrimary,
+    fontSize: 14,
+    //fontWeight: 'bold',
     ...$padding(8, 0, 8)
   },
   infoView: {
     paddingBottom: 8
   },
   infoText: {
-    color: '#444',
-    fontSize: 10
+    color: textCancel,
+    fontSize: 12
   },
   closeIcon: {
     width: 30,
     height: 30,
-    marginTop: -2,
+    marginTop: -16,
     marginRight: -8,
     alignItems: 'center',
     justifyContent: 'center'

+ 504 - 0
Strides-APP/app/pages/my/ProfileV3.js

@@ -0,0 +1,504 @@
+/**
+ * V3版本Profile页面
+ * @邠心vbe on 2024/05/30
+ */
+import React, { Component } from 'react';
+import { View, StyleSheet, Image, ScrollView, StatusBar, Pressable } from 'react-native';
+import Button, { ElevationObject } from '../../components/Button';
+import TextView from '../../components/TextView';
+import Dialog from '../../components/Dialog';
+import apiUser from '../../api/apiUser';
+import { setAccessToken } from '../../api/http';
+import { getStorageJsonSync, setStorage, setStorageJson } from '../../utils/storage';
+import utils from '../../utils/utils';
+import { PageList } from '../Router';
+import app from '../../../app.json';
+import Svg, { Defs, Ellipse, G, LinearGradient, Path, Rect, Stop } from 'react-native-svg';
+import ShadowView from '../../components/ShadowView';
+
+export default class ProfileV3 extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isHide: false,
+      userInfo: userInfo,
+      totalVehicle: 0
+    };
+  }
+
+  componentDidMount() {
+    this.init();
+    this.props.navigation.addListener('focus', () => {
+      this.init();
+      this.setState({
+        isHide: false
+      })
+    });
+    this.props.navigation.addListener('blur', () => {
+      this.setState({
+        isHide: true
+      })
+    });
+  }
+
+  init() {
+    getUserInfo(info => {
+      this.setState({
+        userInfo: info
+      });
+    }, true);
+  }
+
+  deleteAccount() {
+    Dialog.showDialog({
+      title: $t('profile.deleteAccount'),
+      message: $t('profile.confirmDeleteAccount'),
+      ok: $t('nav.confirm'),
+      callback: button => {
+        if (button == Dialog.BUTTON_OK) {
+          this.deleteMyAccount();
+        }
+      }
+    })
+  }
+
+  deleteMyAccount() {
+    Dialog.showProgressDialog();
+    apiUser.deleteAccount().then(res => {
+      toastShort($t('profile.deleteAccountSuccess'))
+      Dialog.dismissLoading();
+      this.requestLogout();
+      /*setTimeout(() => {
+        startPage(PageList.login);
+      }, 500);*/
+    }).catch(err => {
+      Dialog.dismissLoading();
+      toastShort(err)
+    })
+  }
+
+  logout() {
+    Dialog.showDialog({
+      title: $t('profile.signOut'),
+      message: $t('profile.tipSignOut'),
+      callback: btn => {
+        if (btn == Dialog.BUTTON_OK) {
+          Dialog.showProgressDialog();
+          setTimeout(() => {
+            this.requestLogout();
+          }, 500);
+        }
+      }
+    })
+  }
+
+  async requestLogout() {
+    const data = await getStorageJsonSync('loginData');
+    if (data && data.email) {
+      delete data.password
+      setStorageJson('loginData', data);
+      setStorage('RegisterTokenDate', "");
+    }
+    global.userInfo = {}
+    /*this.setState({
+      isLogin: false,
+      userInfo: {}
+    });*/
+    setAccessToken('');
+    goBack();
+    Dialog.dismissLoading();
+  }
+
+  render() {
+    return (
+      <ScrollView style={styles.container}>
+        {/* <StatusBar backgroundColor={this.state.isHide ? colorLight : colorPrimary} /> */}
+        <View style={styles.headerView}>
+          <Pressable
+            style={styles.titleBar}
+            onPress={() => goBack()}>
+            <MaterialIcons
+              name={'arrow-back-ios'}
+              size={20}
+              color={textLight} />
+            <TextView style={styles.titleText}>Profile</TextView>
+          </Pressable>
+          <View style={styles.profileHeader}>
+            {this.state.userInfo.photoUrl
+              ? <Image
+                style={styles.avatarImage}
+                source={{ uri: utils.getImageUrl(this.state.userInfo.photoUrl) }} />
+              : <Image
+                style={styles.avatarImage}
+                source={require('../../images/user/ic-avatar-default.png')} />
+            }
+            <View style={styles.infoContent}>
+              <View style={ui.flexc}>
+                <TextView
+                  style={styles.nickname}
+                  ellipsizeMode='tail'
+                  numberOfLines={1}>{this.state.userInfo.nickName}</TextView>
+                <TextView
+                  style={styles.countryLabel}>Singapore</TextView>
+              </View>
+              <TextView style={styles.userText}>{this.state.userInfo.email}</TextView>
+              <View style={ui.flex}>
+                <Button
+                  text={"Edit"}
+                  textSize={12}
+                  textColor={colorPrimary}
+                  style={styles.editButton}
+                  viewStyle={styles.editButtonView}
+                  iconRight={
+                    <Feather
+                      name="edit-3"
+                      size={14}
+                      color={colorPrimary} />
+                  }
+                  onClick={() => startPage(PageList.editProfile)}
+                />
+              </View>
+            </View>
+          </View>
+        </View>
+        <View style={styles.circleButtomView}>
+          <Svg width={$vw(100)} height={$vw(42.13)} viewBox="0 0 375 158" fill="none">
+            <Ellipse cx={187.5} cy={79} rx={238.5} ry={79} fill={colorPrimary} />
+          </Svg>
+        </View>
+        <Pressable
+          style={styles.walletView}
+          onPress={() => startPage(PageList.transaction)}>
+          <Image
+            style={styles.walletIcon}
+            source={require("../../images/wallet/lumi-logo.png")}/>
+          <View style={styles.walletContent}>
+            <TextView style={styles.walletTitle}>Available Credits:</TextView>
+            <TextView style={styles.walletAmount}>{this.state.userInfo?.creditStr}</TextView>
+          </View>
+          <Button
+            text={"Buy"}
+            textSize={12}
+            textColor={textLight}
+            style={styles.topupButton}
+            viewStyle={styles.topupButtonView}
+            iconRight={
+              <MaterialIcons
+                name="chevron-right"
+                size={16}
+                color={textLight} />
+            }
+            onClick={() => startPage(PageList.topupNew)}/>
+        </Pressable>
+        <ShadowView/>
+        <Button
+          style={styles.cardView}
+          viewStyle={styles.cardItem}
+          onClick={() => startPage(app.vehicle.newVersionPage ? PageList.vehiclesListV2 : PageList.myVehicles)}>
+          {/* <Image
+            style={styles.cardIcon}
+            source={require('../../images/user/card-vehicle.png')}/> */}
+          <View style={styles.cardInfo}>
+            <TextView style={styles.cardLabel}>{$t('profile.myVehicles')}</TextView>
+            {/* <TextView style={styles.cardPrimary}>{this.state.userInfo.countVehicle}</TextView> */}
+          </View>
+          <FontAwesome
+            size={24}
+            color={textPrimary}
+            name='angle-right'/>
+        </Button>
+        {/* <ShadowView/> */}
+        <Button
+          style={styles.cardView}
+          viewStyle={styles.cardItem}
+          onClick={() => {}}>
+          <View style={styles.cardInfo}>
+            <TextView style={styles.cardLabel}>{"Saved Cards"}</TextView>
+          </View>
+          <FontAwesome
+            size={24}
+            color={textPrimary}
+            name='angle-right'/>
+        </Button>
+        {/* <ShadowView/> */}
+        <Button
+          style={styles.cardView}
+          viewStyle={styles.cardItem}
+          onClick={() => startPage(PageList.settings)}>
+          <View style={styles.cardInfo}>
+            <TextView style={styles.cardLabel}>{"App Settings"}</TextView>
+          </View>
+          <FontAwesome
+            size={24}
+            color={textPrimary}
+            name='angle-right'/>
+        </Button>
+        {/* <ShadowView/> */}
+        <Button
+          style={styles.cardView}
+          viewStyle={styles.profileItem}
+          onClick={() => startPage(PageList.changePassword, {action: "change"})}>
+          {/* <Image
+            style={styles.cardIcon}
+            source={require('../../images/user/card-account.png')}/> */}
+          <View style={styles.cardInfo}>
+            <TextView style={styles.cardLabel}>{$t('route.changePassword')}</TextView>
+          </View>
+          <FontAwesome
+            size={24}
+            color={textPrimary}
+            name='angle-right'/>
+        </Button>
+        {/* <ShadowView/> */}
+        <Button
+          style={styles.cardView}
+          viewStyle={styles.profileItem}
+          onClick={() => startPage(PageList.feedback)}>
+          {/* <MaterialCommunityIcons
+            style={styles.cardIcon}
+            name="message-alert-outline"
+            color="#00638C"
+            size={32}
+          /> */}
+          <View style={styles.cardInfo}>
+            <TextView style={styles.cardLabel}>{$t('drawer.feedback')}</TextView>
+          </View>
+          <FontAwesome
+            size={24}
+            color={textPrimary}
+            name='angle-right'/>
+        </Button>
+        {/* <ShadowView/> */}
+        <Button
+          style={styles.cardView}
+          viewStyle={styles.cardItem}
+          onClick={() => {}}>
+          <View style={styles.cardInfo}>
+            <TextView style={styles.cardLabel}>{"Contact Us"}</TextView>
+          </View>
+          <FontAwesome
+            size={24}
+            color={textPrimary}
+            name='angle-right'/>
+        </Button>
+        {/* <ShadowView/> */}
+        <Button
+          style={styles.cardView}
+          viewStyle={styles.profileItem}
+          onClick={() => startPage(PageList.about)}>
+          {/* <MaterialCommunityIcons
+            style={styles.cardIcon}
+            name="information-outline"
+            color="#00638C"
+            size={32}
+          /> */}
+          <View style={styles.cardInfo}>
+            <TextView style={styles.cardLabel}>{$t('drawer.about')}</TextView>
+          </View>
+          <FontAwesome
+            size={24}
+            color={textPrimary}
+            name='angle-right'/>
+        </Button>
+        {/* <ShadowView/> */}
+        <Button
+          style={styles.cardView}
+          viewStyle={styles.cardItem}
+          onClick={() => startPage(PageList.profile)}>
+          <View style={styles.cardInfo}>
+            <TextView style={styles.cardLabel}>{"Check For Updates"}</TextView>
+          </View>
+          <FontAwesome
+            size={24}
+            color={textPrimary}
+            name='angle-right'/>
+        </Button>
+        {/* <ShadowView/> */}
+        <Button
+          style={styles.cardView}
+          viewStyle={styles.profileItem}
+          onClick={() => this.deleteAccount()}>
+          {/* <MaterialCommunityIcons
+            style={styles.cardIcon}
+            name="account-remove"
+            size={32}
+            color="#00638C"/> */}
+          <View style={styles.cardInfo}>
+            <TextView style={styles.cardLabel}>{$t('profile.deleteAccount')}</TextView>
+          </View>
+          <FontAwesome
+            size={24}
+            color={textPrimary}
+            name='angle-right'/>
+        </Button>
+        {/* <ShadowView/> */}
+        <Button
+          style={styles.deleteButton}
+          text={'LOGOUT'}
+          textColor={textLight}
+          onClick={() => this.logout()}
+        />
+        <ShadowView/>
+        <EndView/><EndView/>
+      </ScrollView>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: pageBackground
+  },
+  headerView: {
+    zIndex: 2,
+    backgroundColor: colorPrimary
+  },
+  titleBar: {
+    padding: 16,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  titleText: {
+    color: textLight,
+    fontSize: 20,
+    fontWeight: 'bold'
+  },
+  profileHeader: {
+    padding: 16,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  avatarImage: {
+    width: 80,
+    height: 80,
+    borderWidth: 2,
+    borderRadius: 80,
+    borderColor: colorLight,
+    backgroundColor: colorLight
+  },
+  infoContent: {
+    flex: 1,
+    paddingLeft: 16
+  },
+  nickname: {
+    color: textLight,
+    fontSize: 20,
+    fontWeight: 'bold',
+    paddingTop: 1,
+    paddingBottom: 1
+  },
+  userText: {
+    color: textLight,
+    fontSize: 13,
+    paddingTop: 4
+  },
+  countryLabel: {
+    color: textLight,
+    fontSize: 10,
+    ...$padding(2, 8),
+    marginLeft: 8,
+    borderRadius: 4,
+    backgroundColor: "#f8a300"
+  },
+  editButton: {
+    marginTop: 8,
+    borderRadius: 4,
+    backgroundColor: colorLight
+  },
+  editButtonView: {
+    width: 65,
+    ...$padding(4, 10),
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  circleButtomView: {
+    zIndex: 1,
+    marginTop: -$vw(30)
+  },
+  walletView: {
+    zIndex: 2,
+    padding: 12,
+    paddingRight: 6,
+    marginTop: -$vw(12),
+    marginLeft: 16,
+    marginRight: 16,
+    borderWidth: 2,
+    borderColor: colorPrimary,
+    borderRadius: 6,
+    alignItems: "center",
+    flexDirection: "row",
+    backgroundColor: colorLight,
+  },
+  walletIcon: {
+    width: 48,
+    height: 48
+  },
+  walletContent: {
+    flex: 1,
+    paddingLeft: 8
+  },
+  walletTitle: {
+    color: textPrimary,
+    fontSize: 14,
+    paddingTop: 3
+  },
+  walletAmount: {
+    color: colorPrimary,
+    fontSize: 20,
+    fontWeight: "bold",
+    paddingTop: 2
+  },
+  topupButton: {
+    marginTop: 8,
+    borderRadius: 4,
+    backgroundColor: colorPrimary
+  },
+  topupButtonView: {
+    width: 65,
+    alignItems: 'center',
+    flexDirection: 'row',
+    ...$padding(4, 8, 4, 10)
+  },
+  cardView: {
+    zIndex: 2,
+    //padding: 16,
+    marginTop: 8,
+    marginLeft: 16,
+    marginRight: 16,
+    borderRadius: 10,
+    overflow: 'hidden',
+    alignItems: 'center',
+    flexDirection: 'row',
+    borderWidth: 1,
+    borderColor: "#DADADA",
+    //...ElevationObject(2),
+    backgroundColor: colorLight
+  },
+  cardItem: {
+    padding: 16,
+    alignItems: 'center',
+    flexDirection: 'row',
+    justifyContent: 'center',
+  },
+  cardIcon: {
+    width: 32,
+    height: 32
+  },
+  cardInfo: {
+    flex: 1,
+    //paddingLeft: 16,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  cardLabel: {
+    color: textPrimary,
+    fontSize: 12
+  },
+  deleteButton: {
+    zIndex: 2,
+    borderRadius: 4,
+    backgroundColor: '#EA0A2A',
+    ...$margin(16, 16, 0)
+  }
+})

+ 3 - 3
Strides-APP/app/pages/search/ConnectType.js

@@ -59,7 +59,7 @@ const styles = StyleSheet.create({
   },
   typeLabelV2: {
     color: textPrimary,
-    fontSize: 8,
+    fontSize: 12,
     paddingLeft: 8,
     paddingRight: 4
   },
@@ -72,7 +72,7 @@ const styles = StyleSheet.create({
   typeContentV2: {
     color: textLight,
     height: 20,
-    fontSize: 8,
+    fontSize: 12,
     paddingLeft: 10,
     paddingRight: 8,
     borderRadius: 30,
@@ -85,7 +85,7 @@ const styles = StyleSheet.create({
   },
   typeBoldV2: {
     color: textLight,
-    fontSize: 8,
+    fontSize: 12,
     fontWeight: 'bold'
   }
 })

+ 13 - 13
Strides-APP/app/pages/search/ListViewV3.js

@@ -26,6 +26,7 @@ export default ListViewV3 = ({item, index, separators, onPress, onFavorite}) =>
             <View style={ui.flex1}>
               <TextView style={styles.stationAddress}>{item.address}</TextView>
               <View style={ui.flexc}>
+                <TextView style={[styles.siteTypes, item.siteType != "Public" ? styles.private : {}]}>{item.siteType}</TextView>
                 { (item.allConnector && item.allConnector.available > 0) &&
                   <TextView style={styles.stationAvailable}>
                     <MaterialCommunityIcons
@@ -41,7 +42,7 @@ export default ListViewV3 = ({item, index, separators, onPress, onFavorite}) =>
                 <ConnectTypeV2 {...item?.dcConnector}/>
               </View>
             </View>
-            { item.upcoming
+            {/* item.upcoming
             ? <View style={[ui.center, $margin(0, 8)]}>
                 <MaterialIcons
                   name="upcoming"
@@ -73,19 +74,18 @@ export default ListViewV3 = ({item, index, separators, onPress, onFavorite}) =>
                   size={22}
                   color={colorLight}/>
               </Pressable>
-            </> }
+            </> */}
           </View>
-          <View style={styles.labelRows}>
-            <TextView style={[styles.siteTypes, item.siteType != "Public" ? styles.private : {}]}>{item.siteType}</TextView>
+          {/* <View style={styles.labelRows}>
             {/* item.allConnector && item.allConnector.available > 0 &&
               <TextView style={[styles.infoStatus, styles.available]}>{$t('charging.statusAvailable')}</TextView>
-            */}
+            *}
             { utils.isNotEmpty(item?.labels) &&
               item?.labels.map((label, idx) =>
                 <SiteLabelView {...label} key={idx} version={2}/>
               )
             }
-          </View>
+          </View> */}
         </View>
       </Button>
     );
@@ -99,12 +99,12 @@ const styles = StyleSheet.create({
     borderRadius: 0,
     flexDirection: 'row',
     borderBottomWidth: 1,
-    borderBottomColor: '#eee',
+    borderBottomColor: '#F6F6F6',
     backgroundColor: pageBackground
   },
   itemContent: {
     flex: 1,
-    ...$padding(8, 16),
+    ...$padding(12, 16),
     flexDirection: 'row'
   },
   stationInfo: {
@@ -132,14 +132,14 @@ const styles = StyleSheet.create({
   },
   stationAddress: {
     color: textSecondary,
-    fontSize: 12,
+    fontSize: 14,
     paddingTop: 4,
-    paddingBottom: 4
+    paddingBottom: 6
   },
   siteTypes: {
     color: textLight,
     height: 20,
-    fontSize: 8,
+    fontSize: 12,
     marginRight: 6,
     borderRadius: 30,
     ...$padding(0, 10),
@@ -148,7 +148,7 @@ const styles = StyleSheet.create({
   stationAvailable: {
     color: textPrimary,
     height: 20,
-    fontSize: 8,
+    fontSize: 12,
     marginRight: 6,
     borderRadius: 30,
     borderWidth: 1,
@@ -175,7 +175,7 @@ const styles = StyleSheet.create({
     borderColor: colorAccent
   },
   distance: {
-    fontSize: 8,
+    fontSize: 12,
     color: textLight,
     height: 20,
     fontSize: 10,

+ 2 - 2
Strides-APP/app/pages/search/SearchV3.js

@@ -136,7 +136,7 @@ export default class SearchV3 extends Component {
           <Feather
             name={'search'}
             size={20}
-            color={textCancel}/>
+            color={textSecondary}/>
           <TextInput
             style={styles.searchInput}
             autoFocus={true}
@@ -144,7 +144,7 @@ export default class SearchV3 extends Component {
             numberOfLines={1}
             returnKeyType={'search'}
             clearButtonMode={'while-editing'}
-            placeholder={$t('home.searchHint')}
+            placeholder={"Search by Postal Code, Address, Site Name"}
             placeholderTextColor={textPlacehoder}
             value={this.state.searchWorld}
             onChangeText={text => this.changeWord(text)}

+ 1 - 1
Strides-APP/app/pages/sign/Login.js

@@ -335,7 +335,7 @@ const styles = StyleSheet.create({
     fontSize: 14,
     padding: 4,
     marginTop: 5,
-    fontWeight: 'bold',
+    //fontWeight: 'bold',
     textDecorationLine: 'underline'
   }
 });

+ 28 - 23
Strides-APP/app/pages/sign/ResetPasswordV2.js

@@ -23,6 +23,7 @@ export default class ResetPassword extends Component {
       email: '',
       strength: 0,
       password: '',
+      isChange: false,
       wrongCount: true,
       sendMinutes: 0,
       confirmStatusColor: "#F5F5F5"
@@ -32,22 +33,20 @@ export default class ResetPassword extends Component {
       password: '',
       verificationCode: ''
     }
-    this.isChange = false;
   }
 
   componentDidMount() {
     const action = this.props.route?.params?.action ?? "";
     if (action == "change") {
-      this.isChange = true;
-      /*setTimeout(() => {
-        this.requestLogout();
-      }, 1000);*/
+      this.setState({
+        isChange: true
+      });
+      const email = userInfo.email;
+      this.formInfo.email = email;
+      this.setState({
+        email: email
+      });
     }
-    const email = userInfo.email;
-    this.formInfo.email = email;
-    this.setState({
-      email: email
-    });
   }
 
   componentWillUnmount() {
@@ -165,7 +164,7 @@ export default class ResetPassword extends Component {
     apiUser.updatePassword(this.formInfo).then(res => {
       Dialog.dismissLoading()
       toastShort($t('sign.resetPasswordSuccess'));
-      if (this.isChange) {
+      if (this.state.isChange) {
         this.requestLogout();
       } else {
         goBack();
@@ -225,18 +224,24 @@ export default class ResetPassword extends Component {
                 style={styles.inputIcon}
                 source={require('../../images/user/sign-email.png')}
               />
-              <TextInput
-                style={styles.inputView}
-                placeholder={$t('sign.labelEmail')}
-                placeholderTextColor={textPlacehoder}
-                value={this.state.email}
-                editable={false}
-                maxLength={50}
-                keyboardType="email-address"
-                textContentType='emailAddress'
-                clearButtonMode='while-editing'
-                onChangeText={v => this.changeInfo('email', v)}
-              />
+              { this.state.isChange
+              ? <TextInput
+                  style={styles.inputView}
+                  placeholder={$t('sign.labelEmail')}
+                  placeholderTextColor={textPlacehoder}
+                  value={this.state.email}
+                  editable={false}
+                  maxLength={50}/>
+              : <TextInput
+                  style={styles.inputView}
+                  placeholder={$t('sign.labelEmail')}
+                  placeholderTextColor={textPlacehoder}
+                  maxLength={50}
+                  keyboardType="email-address"
+                  textContentType='emailAddress'
+                  clearButtonMode='while-editing'
+                  onChangeText={v => this.changeInfo('email', v)}/>
+              }
             </View>
             <View style={styles.signInput}>
               {/* <Text style={[styles.inputLabel, ui.flex2]}>Verification Code</Text> */}

+ 365 - 0
Strides-APP/app/pages/signLumi/ForgotPwdVL.js

@@ -0,0 +1,365 @@
+/**
+ * 忘记密码-重置密码LUMI版
+ * @邠心vbe on 2024/06/05
+ */
+import React, { Component } from 'react';
+import { View, ScrollView, StyleSheet, TextInput } from 'react-native';
+import apiUser from '../../api/apiUser';
+import Dialog from '../../components/Dialog';
+import TextView from '../../components/TextView';
+import { getStorageJsonSync, setStorage, setStorageJson } from '../../utils/storage';
+import { PageList } from '../Router';
+
+export default class ForgotPwdVL extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      email: '',
+      strength: 0,
+      password: '',
+      sendMinutes: 0,
+      isChange: false,
+      strengthCheck: {
+        minLength: false,
+        wordCase: false,
+        oneNumber: false,
+        allCheck: false
+      },
+      showPassword: false,
+      confirmStatusColor: "#F5F5F5"
+    };
+    this.formInfo = {
+      email: '',
+      password: '',
+      verificationCode: ''
+    }
+  }
+
+  componentDidMount() {
+    const action = this.props.route?.params?.action ?? "";
+    if (action == "change") {
+      this.setState({
+        isChange: true
+      });
+      const email = userInfo.email;
+      this.formInfo.email = email;
+      this.setState({
+        email: email
+      });
+    }
+  }
+
+  changeInfo(key, value) {
+    this.formInfo[key] = value;
+  }
+
+  applyStrength(text) {
+    const strength = this.state.strengthCheck;
+    strength.allCheck = true;
+    if (text.length >= 8) {
+      strength.minLength = true;
+    } else {
+      strength.minLength = false;
+      strength.allCheck = false;
+    }
+    if (/\d{1,}/.test(text)) {
+      strength.oneNumber = true;
+    } else {
+      strength.oneNumber = false;
+      strength.allCheck = false;
+    }
+    if (/[a-z]{1,}/.test(text) && /[A-Z]{1,}/.test(text)) {
+      strength.wordCase = true;
+    } else {
+      strength.wordCase = false;
+      strength.allCheck = false;
+    }
+    this.setState({
+      password: text,
+      strengthCheck: strength
+    });
+  }
+
+  sendVerification() {
+    var info = this.formInfo;
+    if (!info.email) {
+      toastShort($t('sign.plsInputEmail'));
+      return;
+    }
+    if (!/^[a-zA-Z0-9]+[\S]+@[a-zA-Z0-9_-]+[\.][\Sa-zA-Z]+$/.test(info.email)) {
+      toastShort($t('sign.errEmailFormat'));
+      return;
+    }
+    Dialog.showProgressDialog()
+    apiUser.sendVerificationCode(info.email).then(res => {
+      Dialog.dismissLoading()
+      this.state.sendMinutes = res.data?.resendTime ?? 60;
+      toastShort($t('sign.sendOTPSuccess'));
+      this.contdownTime();
+    }).catch(err => {
+      Dialog.dismissLoading()
+      toastShort(err);
+    });
+  }
+
+  contdownTime() {
+    if (this.state.sendMinutes > 0) {
+      this.setState({
+        sendMinutes: this.state.sendMinutes - 1
+      })
+      setTimeout(() => {
+        this.contdownTime();
+      }, 1000);
+    }
+  }
+
+  onResetPassword() {
+    var info = this.formInfo;
+    console.log('reset info', info);
+    if (!info.email) {
+      toastShort($t('sign.plsInputEmail'));
+      return;
+    }
+    if (!/^[a-zA-Z0-9]+[\S]+@[a-zA-Z0-9_-]+[\.][\Sa-zA-Z]+$/.test(info.email)) {
+      toastShort($t('sign.errEmailFormat'));
+      return;
+    }
+    /*if (!info.verificationCode) {
+      toastShort($t('sign.plsInputOTP'));
+      return;
+    }*/
+    if (!this.state.password) {
+      toastShort($t('sign.plsInputPassword'));
+      return;
+    }
+    if (!this.state.strengthCheck.allCheck) {
+      toastShort($t('sign.errPasswordStrong'));
+      return;
+    }
+    if (!info.password) {
+      toastShort($t('sign.plsInputPassword2'));
+      return;
+    }
+    if (info.password != this.state.password) {
+      toastShort($t('sign.errPasswordConfirm'));
+      return;
+    }
+    Dialog.showProgressDialog()
+    apiUser.updatePassword(this.formInfo).then(res => {
+      Dialog.dismissLoading()
+      toastShort($t('sign.resetPasswordSuccess'));
+      if (this.isChange) {
+        this.requestLogout();
+      } else {
+        goBack();
+      }
+    }).catch(err => {
+      Dialog.dismissLoading()
+      toastShort(err);
+    });
+  }
+
+  async requestLogout() {
+    const data = await getStorageJsonSync('loginData');
+    if (data && data.email) {
+      delete data.password
+      setStorageJson('loginData', data);
+      setStorage('RegisterTokenDate', "");
+    }
+    global.userInfo = {}
+    /*this.setState({
+      isLogin: false,
+      userInfo: {}
+    });*/
+    setAccessToken('');
+    routeUtil.bridge2Page(PageList.login);
+  }
+
+  changeSecurety() {
+    this.setState({
+      showPassword: !this.state.showPassword
+    })
+  }
+
+  render() {
+    return (
+      <ScrollView
+        style={styles.scollView}
+        keyboardShouldPersistTaps={isIOS ? 'never' : 'handled'}>
+        <View style={styles.resetView}>
+          <TextView style={styles.inputLabel}>Email Address</TextView>
+          <View style={styles.signInput}>
+            { this.state.isChange
+            ? <TextInput
+                style={styles.inputView}
+                placeholder={$t('sign.labelEmail')}
+                placeholderTextColor={textPlacehoder}
+                value={this.state.email}
+                editable={false}
+                maxLength={50}/>
+            : <TextInput
+                style={styles.inputView}
+                placeholder={$t('sign.labelEmail')}
+                placeholderTextColor={textPlacehoder}
+                maxLength={50}
+                keyboardType="email-address"
+                textContentType='emailAddress'
+                clearButtonMode='while-editing'
+                onChangeText={v => this.changeInfo('email', v)}/>
+            }
+          </View>
+          {/* <Text style={styles.inputLabel}>Verification Code</Text>
+          <View style={ui.flexc}>
+            <View style={[styles.signInput, ui.flex2]}>
+              <TextInput
+                style={[styles.inputView, {flex: 2.6, marginLeft: 2}]}
+                placeholder={$t('sign.labelOtp')}
+                placeholderTextColor={textPlacehoder}
+                maxLength={6}
+                keyboardType="number-pad"
+                textContentType="telephoneNumber"
+                onChangeText={v => this.changeInfo('verificationCode', v)}
+              />
+            </View>
+            <Button
+              text={this.state.sendMinutes > 0 ? (this.state.sendMinutes + " s") : $t('sign.btnSendOTP')}
+              style={styles.sendBtn}
+              disabled={this.state.sendMinutes > 0}
+              viewStyle={styles.sendBtnView}
+              textStyle={styles.sendBtnText}
+              onClick={() => this.sendVerification()}/>
+          </View> */}
+          <TextView style={styles.inputLabel}>New Password</TextView>
+          <View style={styles.signInput}>
+            <TextInput
+              secureTextEntry={!this.state.showPassword}
+              style={styles.inputView}
+              placeholder={$t('sign.labelNewPassword')}
+              placeholderTextColor={textPlacehoder}
+              maxLength={20}
+              onChangeText={(value) => this.applyStrength(value)}/>
+            <MaterialCommunityIcons
+              name={this.state.showPassword ? "eye" : "eye-off"}
+              size={16}
+              color={"#DADADA"}
+              onPress={() => this.changeSecurety()}/>
+          </View>
+          <View style={styles.passwordTipView}>
+            <View style={ui.flexc}>
+              <MaterialIcons
+                name="check-circle-outline"
+                color={this.state.strengthCheck.minLength ? colorAccent : colorCancel}
+                size={18}/>
+              <TextView style={styles.passwordTipText}>8 or more characters</TextView>
+            </View>
+            <View style={ui.flexc}>
+              <MaterialIcons
+                name="check-circle-outline"
+                color={this.state.strengthCheck.wordCase ? colorAccent : colorCancel}
+                size={18}/>
+              <TextView style={styles.passwordTipText}>Upper and Lower case letters</TextView>
+            </View>
+            <View style={ui.flexc}>
+              <MaterialIcons
+                name="check-circle-outline"
+                color={this.state.strengthCheck.oneNumber ? colorAccent : colorCancel}
+                size={18}/>
+              <TextView style={styles.passwordTipText}>At least one number</TextView>
+            </View>
+          </View>
+          <TextView style={styles.inputLabel}>{$t('sign.labelConfirmPassword')}</TextView>
+          <View style={styles.signInput}>
+            <TextInput
+              secureTextEntry={!this.state.showPassword}
+              style={styles.inputView}
+              placeholder={$t('sign.labelConfirmPassword')}
+              placeholderTextColor={textPlacehoder}
+              maxLength={20}
+              onChangeText={v => this.changeInfo('password', v)}/>
+            <MaterialCommunityIcons
+              name={this.state.showPassword ? "eye" : "eye-off"}
+              size={16}
+              color={"#DADADA"}
+              onPress={() => this.changeSecurety()}/>
+          </View>
+          <Button
+            text={$t('common.confirm')}
+            style={styles.resetButton}
+            onClick={() => this.onResetPassword()}
+          />
+        </View>
+      </ScrollView>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  scollView: {
+    flex: 1,
+    backgroundColor: colorLight
+  },
+  resetView: {
+    padding: 16,
+    backgroundColor: colorLight
+  },
+  inputLabel: {
+    color: textPrimary,
+    fontSize: 14,
+    paddingBottom: 8
+  },
+  signInput: {
+    marginBottom: 16,
+    borderRadius: 0,
+    paddingLeft: 16,
+    paddingRight: 16,
+    alignItems: 'center',
+    flexDirection: 'row',
+    borderWidth: 1,
+    borderRadius: 4,
+    borderColor: "#DADADA"
+  },
+  inputView: {
+    flex: 1,
+    height: 48,
+    color: textPrimary,
+    fontSize: 12
+  },
+  sendBtn: {
+    flex: 1.2,
+    marginLeft: 16,
+    marginRight: 0,
+    marginBottom: 16,
+    borderRadius: 6,
+    backgroundColor: colorPrimary
+  },
+  sendBtnView: {
+    flex: 1,
+    height: 48,
+    paddingLeft: 4,
+    paddingRight: 4,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  sendBtnText: {
+    color: textButton,
+    fontSize: 13,
+    fontWeight: 'bold'
+  },
+  passwordTipView: {
+    marginTop: -8,
+    paddingBottom: 16
+  },
+  passwordTipText: {
+    color: "#666",
+    fontSize: 14,
+    paddingTop: 2,
+    paddingLeft: 8,
+    paddingBottom: 2
+  },
+  resetButton: {
+    marginTop: 32,
+    marginBottom: 16,
+    borderRadius: 4,
+    backgroundColor: colorPrimary
+  }
+})

+ 316 - 0
Strides-APP/app/pages/signLumi/LoginVL.js

@@ -0,0 +1,316 @@
+/**
+ * LUMI版本Login页面
+ * @邠心vbe on 2024/05/31
+ */
+import React from 'react';
+import { BackHandler, Image, ScrollView, StyleSheet, TextInput, View } from 'react-native';
+import { PageList } from '../Router';
+import apiUser from '../../api/apiUser';
+import Button from '../../components/Button';
+import Dialog from '../../components/Dialog';
+import { setAccessToken } from '../../api/http';
+import TextView from '../../components/TextView';
+import { BackButton } from '../../components/Toolbar';
+import CheckBoxText from '../../components/CheckBoxText';
+import { getStorageJsonSync, setStorageJson } from '../../utils/storage';
+
+export default class Login extends React.Component {
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      email: '',
+      password: '',
+      rememberMe: true,
+      showPassword: false
+    }
+    this.isHide = false;
+  }
+
+  componentDidMount() {
+    BackHandler.addEventListener('hardwareBackPress', this.toExit)
+    this.props.navigation.addListener('focus', () => {
+      this.isHide = false;
+    });
+
+    this.props.navigation.addListener('blur', () => {
+      this.isHide = true;
+    });
+
+    //this.getEmail();
+  }
+
+  componentWillUnmount() {
+    BackHandler.removeEventListener("hardwareBackPress", this.toExit)
+  }
+
+  toExit = () => {
+    if (!this.isHide)
+      return true;
+  }
+
+  async getEmail() {
+    const data = await getStorageJsonSync('loginData');
+    if (data && data.email) {
+      this.setState({
+        email: data.email
+      });
+    }
+  }
+
+  onLogin() {
+    //console.log(this.state);
+    if (this.state.email == '') {
+      toastShort($t('sign.plsInputEmail'));
+      return;
+    }
+    if (this.state.password == '') {
+      toastShort($t('sign.plsInputPassword'));
+      return;
+    }
+    Dialog.showProgressDialog();
+    apiUser.login(this.state).then(res => {
+      console.log('res.data', res);
+      if (res.data.accessToken) {
+        setAccessToken(res.data.accessToken);
+        Dialog.dismissLoading();
+        if (this.state.rememberMe) {
+          setStorageJson('loginData', this.state);
+        } else {
+          setStorageJson('loginData', {});
+        }
+        startPage(PageList.home);
+        //this.props.navigation.goBack();
+      } else {
+        toastShort(res.msg);
+        Dialog.dismissLoading();
+      }
+    }).catch(err => {
+      toastShort(err);
+      Dialog.dismissLoading();
+    });
+  }
+
+  getBackTopPosition() {
+    return isIOS ? statusHeight : 4;
+  }
+
+  togglePassword() {
+    this.setState({
+      showPassword: !this.state.showPassword
+    })
+  }
+
+  render() {
+    return (
+      <View style={ui.flex1}>
+      {/* <View style={[styles.backBtn, {top: this.getBackTopPosition()}]}>
+        <BackButton/>
+      </View> */}
+      <ScrollView
+        style={styles.container}
+        keyboardShouldPersistTaps={isIOS ? 'never' : 'handled'}>
+        <View style={styles.header}>
+          <Image
+            style={styles.headerImg}
+            resizeMode='contain'
+            source={require('../../images/app-logo.png')} />
+        </View>
+        <View style={styles.loginView}>
+          <View style={ui.center}>
+            {/* <Image 
+              style={styles.logoImg}
+              resizeMode='contain'
+              source={require('../../images/app-logo.png')} /> */}
+            <TextView style={styles.loginTitle}>{$t('sign.plsLoginTitle')}</TextView>
+          </View>
+          <View style={styles.loginForm}>
+            <View style={styles.inputView}>
+              {/* <Zocial name='email' size={28} color='#999999' /> */}
+              {/* <Image 
+                style={styles.inputIcon}
+                source={require('../../images/user/sign-email.png')}
+              /> */}
+              <TextInput
+                style={styles.inputText}
+                placeholder={"example@email.com"}
+                placeholderTextColor={textPlacehoder}
+                keyboardType="email-address"
+                textContentType='emailAddress'
+                defaultValue={this.state.email}
+                maxLength={50}
+                clearButtonMode='while-editing'
+                autoCapitalize="none"
+                autoComplete="off"
+                autoCorrect={false}
+                onChangeText={(v) => {
+                  this.setState({
+                    email: v
+                  })
+                }}/>
+            </View>
+            <View style={styles.inputView}>
+              {/* <Fontisto name='locked' size={28} color='#999999' style={{marginLeft: 3, marginRight: 2}} /> */}
+              {/* <Image 
+                style={styles.inputIcon}
+                source={require('../../images/user/sign-password.png')}
+              /> */}
+              <TextInput 
+                style={styles.inputText}
+                placeholder={$t('sign.labelPassword')}
+                placeholderTextColor={textPlacehoder}
+                textContentType='password'
+                secureTextEntry={!this.state.showPassword}
+                maxLength={20}
+                onChangeText={(v) => {
+                  this.setState({
+                    password: v
+                  })
+                }}
+                onSubmitEditing={() => {
+                  //this.onLogin();
+                }}/>
+              <MaterialCommunityIcons
+                name={this.state.showPassword ? 'eye' : 'eye-off' }
+                size={14}
+                color={"#ccc"}
+                onPress={() => this.togglePassword()}/>
+            </View>
+            <View style={ui.flexcw}>
+              <View style={$padding(12, 8)}>
+                <CheckBoxText
+                  value={this.state.rememberMe}
+                  onValueChange={(newValue) => {
+                    this.setState({ rememberMe: newValue });
+                  }}
+                  text={$t('sign.rememberMe')}
+                />
+              </View>
+              <TextView
+                style={styles.linksText}
+                onPress={() => startPage(PageList.forgotPasswordLumi)}>{$t('sign.forgotPassword')}</TextView>
+            </View>
+            <Button
+              style={styles.loginButton}
+              text={$t('sign.btnSignIn')}
+              elevation={1.5}
+              onClick={() => {
+                this.onLogin()
+              }}
+            />
+          </View>
+        </View>
+        <View style={styles.signView}>
+          <TextView style={{color: textPrimary}}>{$t('sign.tipNewUser')}</TextView>
+          {/* <Text
+            style={styles.linksText}
+            onPress={() => {
+              startPage(PageList.register);
+            }}
+          >Click here to sign up</Text> */}
+          <TextView 
+            style={styles.linksText}
+            onPress={() => startPage(PageList.registerLumi)}>{$t('sign.registerPublicUser')}</TextView>
+          {/* <Text 
+            style={styles.linksText}
+            onPress={() => startPage(PageList.register, {isFleetUser: true})}>{$t('sign.registerDriverUser')}</Text> */}
+        </View>
+      </ScrollView>
+      </View>
+    );
+  }
+};
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: colorLight
+  },
+  backBtn:{
+    top: 4,
+    left: 2,
+    zIndex: 1,
+    position: 'absolute'
+  },
+  header: {
+    paddingTop: 22,
+    paddingBottom: 20,
+    paddingLeft: 32,
+    paddingRight: 32,
+    alignItems: 'center',
+    backgroundColor: colorPrimary
+  },
+  headerImg: {
+    width: $vw(65),
+    height: $vw(56.118)
+  },
+  loginView: {
+    padding: 16,
+    marginTop: -24,
+    borderTopLeftRadius: 24,
+    borderTopRightRadius: 24,
+    backgroundColor: colorLight
+  },
+  logoImg: {
+    width:136.19,
+    height: 42.85,
+    marginTop: 18
+  },
+  loginForm: {
+    paddingLeft: 16,
+    paddingRight: 16,
+  },
+  loginTitle: {
+    fontSize: 28,
+    fontWeight: 'bold',
+    color: textPrimary
+  },
+  inputIcon: {
+    width: 24,
+    height: 24
+  },
+  inputView: {
+    marginTop: 30,
+    borderRadius: 0,
+    paddingTop: 6,
+    paddingLeft: 16,
+    paddingRight: 16,
+    paddingBottom: 6,
+    alignItems: 'center',
+    flexDirection: 'row',
+    borderWidth: 1,
+    borderColor: '#DADADA',
+    borderRadius: 4
+  },
+  inputText: {
+    flex: 1,
+    color: textPrimary,
+    fontSize: 12,
+    paddingTop: 6,
+    paddingLeft: 6,
+    paddingBottom: 6
+  },
+  loginButton: {
+    marginTop: 32,
+    borderRadius: 4,
+    backgroundColor: colorPrimary
+  },
+  signView: {
+    paddingTop: 32,
+    paddingBottom: 16,
+    alignItems: 'center',
+  },
+  checkText: {
+    color: textPrimary,
+    fontSize: 14,
+    paddingLeft: 8
+  },
+  linksText: {
+    ...ui.link,
+    fontSize: 14,
+    padding: 4,
+    marginTop: 5,
+    //fontWeight: 'bold',
+    textDecorationLine: 'underline'
+  }
+});

+ 531 - 0
Strides-APP/app/pages/signLumi/RegisterVL.js

@@ -0,0 +1,531 @@
+/**
+ * LUMI版本Register页面
+ * @邠心vbe on 2024/05/31
+ */
+import React, { Component } from 'react';
+import { View, ScrollView, StyleSheet, TextInput, Pressable } from 'react-native';
+import apiUser from '../../api/apiUser';
+import Button from '../../components/Button';
+import { PageList } from '../Router';
+import Dialog from '../../components/Dialog';
+import Dropdown from '../../components/Dropdown';
+import { CountryDropNum, GetCountryList } from '../../components/CountryIcon';
+import CheckBox from '../../components/CheckBox';
+import TextView from '../../components/TextView';
+
+export default class RegisterVL extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      agree: true,
+      countryNum: '65',
+      countryCode: "SG",
+      userInfo: {
+        email: "",
+        phone: "",
+        vehicle: {},
+        password: "",
+        verificationCode: ""
+      },
+      countryList: [],
+      params: {...this.props.route.params},
+      email: '',
+      password: '',
+      sendMinutes: 0,
+      strengthCheck: {
+        minLength: false,
+        wordCase: false,
+        oneNumber: false,
+        allCheck: false
+      },
+      showPassword: false
+    };
+  }
+
+  componentDidMount() {
+    this.getCountryList();
+  }
+
+  togglePassword() {
+    this.setState({
+      showPassword: !this.state.showPassword
+    })
+  }
+
+  applyStrength(text) {
+    const strength = this.state.strengthCheck;
+    strength.allCheck = true;
+    if (text.length >= 8) {
+      strength.minLength = true;
+    } else {
+      strength.minLength = false;
+      strength.allCheck = false;
+    }
+    if (/\d{1,}/.test(text)) {
+      strength.oneNumber = true;
+    } else {
+      strength.oneNumber = false;
+      strength.allCheck = false;
+    }
+    if (/[a-z]{1,}/.test(text) && /[A-Z]{1,}/.test(text)) {
+      strength.wordCase = true;
+    } else {
+      strength.wordCase = false;
+      strength.allCheck = false;
+    }
+    this.setState({
+      password: text,
+      strengthCheck: strength
+    });
+  }
+
+  changeInfo(key, value) {
+    var info = this.state.userInfo;
+    info[key] = value;
+    this.setState({
+      'userInfo': info
+    });
+  }
+
+  changeAgree(ag) {
+    this.setState({
+      agree: ag
+    })
+  }
+
+  changeCountry(value, index) {
+    this.setState({
+      countryCode: value
+    })
+    if (this.canChangeCalling) {
+      const country = this.state.countryList[index]
+      this.changeCalling(country.countryNum, -1);
+    }
+  }
+
+  changeCalling(value, index) {
+    this.setState({
+      countryNum: value
+    })
+    if (index >= 0) {
+      this.canChangeCalling = false;
+    }
+  }
+
+  getCountryList() {
+    GetCountryList(list => {
+      this.setState({
+        countryList: list
+      })
+    })
+  }
+
+  sendVerification() {
+    var info = this.state.userInfo;
+    if (!info.email) {
+      toastShort($t('sign.plsInputEmail'));
+      return;
+    }
+    if (!/^[a-zA-Z0-9]+[\S]+@[a-zA-Z0-9_-]+[\.][\Sa-zA-Z]+$/.test(info.email)) {
+      toastShort($t('sign.errEmailFormat'));
+      return;
+    }
+    Dialog.showProgressDialog()
+    apiUser.sendVerificationCodeV2({email: info.email, type: "register"}).then(res => {
+      Dialog.dismissLoading()
+      //this.state.sendMinutes = 60;
+      this.state.sendMinutes = res.data?.resendTime ?? 60;
+      toastShort($t('sign.sendOTPSuccess'));
+      this.contdownTime();
+    }).catch(err => {
+      Dialog.dismissLoading()
+      toastShort(err);
+    });
+  }
+
+  contdownTime() {
+    if (this.state.sendMinutes > 0) {
+      this.setState({
+        sendMinutes: this.state.sendMinutes - 1
+      })
+      setTimeout(() => {
+        this.contdownTime();
+      }, 1000);
+    }
+  }
+
+  onRegister() {
+    //console.log('sign up', this.state);
+    var info = this.state.userInfo;
+    if (!info.nickName) {
+      toastShort($t('sign.plsInputDiaplayName'));
+      return;
+    }
+    if (!info.email) {
+      toastShort($t('sign.plsInputEmail'));
+      return;
+    }
+    if (!/^[a-zA-Z0-9]+[\S]+@[a-zA-Z0-9_-]+[\.][\Sa-zA-Z]+$/.test(info.email)) {
+      toastShort($t('sign.errEmailFormat'));
+      return;
+    }
+    /*if (!info.verificationCode) {
+      toastShort($t('sign.plsInputOTP'));
+      return;
+    }*/
+    if (!info.phone) {
+      toastShort($t('sign.plsInputContactNo'));
+      return;
+    }
+    if (!/^\d{6,15}$/.test(info.phone)) {
+      toastShort($t('sign.errContactNoFormat'));
+      return;
+    }
+    if (!this.state.password) {
+      toastShort($t('sign.plsInputPassword'));
+      return;
+    }
+    if (!this.state.strengthCheck.allCheck) {
+      toastShort($t('sign.errPasswordStrong'));
+      return;
+    }
+    if (!info.password) {
+      toastShort($t('sign.plsInputPassword2'));
+      return;
+    }
+    if (info.password != this.state.password) {
+      toastShort($t('sign.errPasswordConfirm'));
+      return;
+    }
+    if (this.state.isFleetDriver) {
+      if (!info.pdvLicence) {
+        toastShort($t('sign.plsInputPDVLicence'));
+        return;
+      }
+      if (this.state.pdvImages[0] == '' || this.state.pdvImages[1] == '') {
+        toastShort($t('sign.plsUploadLicencePhotos'));
+        return;
+      }
+    }
+    let param = Object.assign({}, info);
+    //param.phone = this.state.countryNum + info.phone
+    param.callingCode = this.state.countryNum;
+    param.countryCode = this.state.countryCode;
+    if (this.state.isFleetDriver) {
+      param.userType = 'Driver';
+      param.pdvLicencePictures = this.state.pdvImages;
+      param.fleetCompanyId = this.state.fleetCompanyId;
+    } else {
+      param.userType = 'Public';
+    }
+    param.otp = param.verificationCode;
+    param.vehicle = this.state.vehicleInfo;
+    console.log('params', param);
+    Dialog.showProgressDialog();
+    apiUser.register(param).then(res => {
+      Dialog.dismissLoading();
+      //toastShort('Sign up successfully!');
+      if (isIOS) {
+        setTimeout(() => {
+          this.showSuccessDialog();
+        }, 600);
+      } else {
+        this.showSuccessDialog();
+      }
+    }).catch(err => {
+      toastShort(err);
+      Dialog.dismissLoading();
+    });
+  }
+
+  render() {
+    return (
+      <ScrollView
+        style={styles.container}
+        contentContainerStyle={$padding(16)}
+        keyboardShouldPersistTaps={isIOS ? 'never' : 'handled'}>
+        {/* <TextView style={styles.textTitle}>Registration details</TextView>
+        <TextView style={styles.textSubTitle}>Please input relevant info as stated</TextView> */}
+        <View style={styles.signInput}>
+          <TextInput
+            style={styles.inputView} 
+            placeholder={$t('sign.labelDisplayName')}
+            placeholderTextColor={textPlacehoder}
+            maxLength={50}
+            onChangeText={v => this.changeInfo('nickName', v)}
+          />
+        </View>
+        <View style={styles.signInput}>
+          <TextInput
+            style={styles.inputView}
+            placeholder={$t('sign.labelEmail')}
+            placeholderTextColor={textPlacehoder}
+            maxLength={50}
+            keyboardType="email-address"
+            textContentType='emailAddress'
+            onChangeText={v => this.changeInfo('email', v)}
+          />
+        </View>
+        {/* <View style={ui.flexc}>
+          <View style={[styles.signInput, ui.flex2]}>
+            <TextInput
+              style={[styles.inputView, {flex: 2.2, marginLeft: 2}]}
+              placeholder={"Validate OTP"}
+              placeholderTextColor={textPlacehoder}
+              maxLength={6}
+              keyboardType="number-pad"
+              textContentType="telephoneNumber"
+              onChangeText={v => this.changeInfo('verificationCode', v)}
+            />
+          </View>
+          <Button
+            text={this.state.sendMinutes > 0 ? (this.state.sendMinutes + " s") : $t('sign.btnSendOTP')}
+            style={styles.sendBtn}
+            disabled={this.state.sendMinutes > 0}
+            viewStyle={styles.sendBtnView}
+            textStyle={styles.sendBtnText}
+            onClick={() => this.sendVerification()}
+          />
+        </View> */}
+        <View style={ui.flexc}>
+          <View style={[styles.signInput, styles.dropView]}>
+            <TextInput style={styles.dropInput} editable={false}/>
+            <TextView style={styles.countryText}>{"+" + this.state.countryNum}</TextView>
+            <MaterialIcons name={'keyboard-arrow-down'} size={24} color={colorDark}/>
+            <Dropdown
+              style={styles.dropLayer}
+              prefixText="+"
+              list={this.state.countryList}
+              value={this.state.countryNum}
+              nameKey='countryNum'
+              valueKey='countryNum'
+              onChange={(value, index)=> this.changeCalling(value, index)}
+              customerItemView={
+                (item, index, onClick) => 
+                <CountryDropNum
+                  key={index} 
+                  country={item}
+                  value={this.state.countryNum}
+                  onClick={onClick}/>
+              }/>
+          </View>
+          <View style={[styles.signInput, ui.flex2]}>
+            <TextInput
+              style={styles.inputView}
+              placeholder={$t('sign.labelMobileNumber')}
+              placeholderTextColor={textPlacehoder}
+              keyboardType='phone-pad'
+              maxLength={15}
+              onChangeText={v => this.changeInfo('phone', v)}
+            />
+          </View>
+        </View>
+        <View style={styles.signInput}>
+          <TextInput
+            secureTextEntry={!this.state.showPassword}
+            style={styles.inputView}
+            placeholder={$t('sign.labelPassword')}
+            placeholderTextColor={textPlacehoder}
+            maxLength={20}
+            onChangeText={(value) => this.applyStrength(value)}/>
+          <MaterialCommunityIcons
+            name={this.state.showPassword ? "eye" : "eye-off"}
+            size={16}
+            color={"#DADADA"}
+            onPress={() => this.togglePassword()}/>
+        </View>
+        <View style={styles.passwordTipView}>
+          {/* <TextView style={styles.passwordTipText}>Your password must have:</TextView> */}
+          <View style={ui.flexc}>
+            <MaterialIcons
+              name="check-circle-outline"
+              color={this.state.strengthCheck.minLength ? colorAccent : colorCancel}
+              size={18}/>
+            <TextView style={styles.passwordTipText}>8 or more characters</TextView>
+          </View>
+          <View style={ui.flexc}>
+            <MaterialIcons
+              name="check-circle-outline"
+              color={this.state.strengthCheck.wordCase ? colorAccent : colorCancel}
+              size={18}/>
+            <TextView style={styles.passwordTipText}>Upper and Lower case letters</TextView>
+          </View>
+          <View style={ui.flexc}>
+            <MaterialIcons
+              name="check-circle-outline"
+              color={this.state.strengthCheck.oneNumber ? colorAccent : colorCancel}
+              size={18}/>
+            <TextView style={styles.passwordTipText}>At least one number</TextView>
+          </View>
+        </View>
+        <View style={styles.signInput}>
+          <TextInput
+            secureTextEntry={!this.state.showPassword}
+            style={styles.inputView}
+            placeholder={$t('sign.labelConfirmPassword')}
+            placeholderTextColor={textPlacehoder}
+            maxLength={20}
+            onChangeText={v => this.changeInfo('password', v)}/>
+          <MaterialCommunityIcons
+            name={this.state.showPassword ? "eye" : "eye-off"}
+            size={16}
+            color={"#DADADA"}
+            onPress={() => this.togglePassword()}/>
+        </View>
+        <EndView/><EndView half/>
+        {/* <TextView style={styles.textTitle}>Got A Referral Code?</TextView>
+        <TextView style={styles.textSubTitle}>Please input the code from your referrer</TextView>
+        <View style={styles.signInput}>
+          <TextInput
+            style={styles.inputView}
+            placeholder={"Referral Code"}
+            placeholderTextColor={textPlacehoder}
+            maxLength={6}
+            onChangeText={v => this.changeInfo('referralCode', v)}/>
+        </View> */}
+        <View style={styles.agreeView}>
+          <CheckBox
+            value={this.state.agree}
+            onValueChange={v => this.changeAgree(v)}
+          />
+          <View style={styles.agreeTextRow}>
+            <TextView style={styles.agreeText} onPress={() => this.changeAgree(!this.state.agree)}>
+              {$t('sign.iHaveReadAndAgree')}
+            </TextView>
+            <TextView style={styles.agreeLink} onPress={() => startPage(PageList.condition)}>{$t('drawer.termsOfUse')}</TextView>
+            <TextView style={styles.agreeText}>{' '}</TextView>
+            <TextView style={styles.agreeText}>{$t('sign.linkAndLink')}</TextView>
+            <TextView style={styles.agreeLink} onPress={() => startPage(PageList.privacy)}>{$t('drawer.privacyPolicy')}</TextView>
+            <TextView style={styles.agreeText}>{$t('sign.linkAndLinkEnd')}</TextView>
+          </View>
+        </View>
+        <Button
+          style={styles.signButton}
+          elevation={0}
+          disabled={!this.state.agree}
+          text={$t('sign.btnRegister')}
+          fontSize={14}
+          onClick={() => this.onRegister()}/>
+      </ScrollView>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: colorLight
+  },
+  textTitle: {
+    color: textPrimary,
+    fontSize: 16,
+    fontWeight: "bold"
+  },
+  textSubTitle: {
+    color: textSecondary,
+    fontSize: 12
+  },
+  signInput: {
+    marginTop: 16,
+    borderRadius: 0,
+    paddingLeft: 16,
+    paddingRight: 16,
+    alignItems: 'center',
+    flexDirection: 'row',
+    borderWidth: 1,
+    borderRadius: 4,
+    borderColor: "#DADADA"
+  },
+  inputView: {
+    flex: 1,
+    height: 48,
+    color: textPrimary,
+    fontSize: 12
+  },
+  sendBtn: {
+    flex: 1.2,
+    marginLeft: 16,
+    marginRight: 0,
+    marginTop: 16,
+    borderRadius: 6,
+    backgroundColor: colorPrimary
+  },
+  sendBtnView: {
+    flex: 1,
+    height: 48,
+    paddingLeft: 4,
+    paddingRight: 4,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  sendBtnText: {
+    color: textButton,
+    fontSize: 13,
+    fontWeight: 'bold'
+  },
+  dropView: {
+    flex: 1,
+    marginRight: 16,
+    paddingRight: 8,
+    alignItems: "center",
+    flexDirection: "row"
+  },
+  dropLayer: {
+    left: 0,
+    right: 0,
+    opacity: 0,
+    position: 'absolute'
+  },
+  dropInput: {
+    width: 8,
+    color: textPrimary,
+    height: 48
+  },
+  countryText: {
+    flex: 1,
+    color: "#9D9D9D",
+    fontSize: 14,
+    paddingRight: 4
+  },
+  passwordTipView: {
+    paddingTop: 8
+  },
+  passwordTipText: {
+    color: "#666",
+    fontSize: 14,
+    paddingTop: 2,
+    paddingLeft: 8,
+    paddingBottom: 2
+  },
+  agreeView: {
+    marginTop: 48,
+    marginBottom: 16,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  agreeTextRow: {
+    flex: 1,
+    paddingTop: 4,
+    paddingLeft: 8,
+    flexWrap: 'wrap',
+    flexDirection: 'row'
+  },
+  agreeText: {
+    color: textPrimary,
+    fontSize: 14,
+    paddingTop: 2,
+    paddingBottom: 2
+  },
+  agreeLink: {
+    ...ui.link,
+    fontSize: 14,
+    paddingTop: 2,
+    paddingBottom: 2,
+    textDecorationLine: 'underline'
+  },
+  signButton: {
+    marginBottom: 24,
+    borderRadius: 4,
+    backgroundColor: colorPrimary
+  }
+})

+ 220 - 0
Strides-APP/app/pages/transaction/HistoryList.js

@@ -0,0 +1,220 @@
+/**
+ * 交易历史页面
+ * @邠心vbe on 2024/03/29
+ */
+import React, { Component } from 'react';
+import { View, Text, RefreshControl, FlatList, StyleSheet, Pressable, Image } from 'react-native';
+import { MyRefreshProps } from '../../components/ThemesConfig';
+import apiWallet from '../../api/apiWallet';
+import TextView from '../../components/TextView';
+import Dialog from '../../components/Dialog';
+import { PageList } from '../Router';
+
+const IconCharge = require('../../images/wallet/ic-type-charge.png');
+const IconPayment = require('../../images/wallet/ic-type-payment.png');
+
+export default class HistoryList extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      dataList: [],
+      hasMore: true,
+      refreshing: false
+    };
+  }
+
+  componentDidMount() {
+    Dialog.showProgressDialog();
+    this.getHistoryList();
+  }
+
+  onRefresh() {
+    this.setState({
+      refreshing: true
+    })
+    this.getHistoryList();
+  }
+
+  getNextPage() {
+    if (this.state.dataList.length > 0 && this.state.hasMore) {
+      console.log("[Wallet History]", "getNextPage");
+      const last = this.state.dataList[this.state.dataList.length-1]
+      this.getHistoryList(last.creditRecordPk);
+    }
+  }
+
+  getHistoryList(lastPk="") {
+    apiWallet.getTransactionListV2({latestPk: lastPk}).then(res => {
+      if (res.data) {
+        if (lastPk) {
+          if (res.data.length > 0) {
+            const list = this.state.dataList;
+            this.setState({
+              dataList: list.concat(res.data)
+            });
+          } else {
+            this.setState({
+              hasMore: false
+            })
+          }
+        } else {
+          this.setState({
+            dataList: res.data,
+            hasMore: true
+          });
+        }
+      } else {
+        this.setState({
+          dataList: []
+        });
+      }
+    }).catch(err => {
+      toastShort(err)
+    }).finally(() => {
+      this.setState({
+        refreshing: false
+      });
+      Dialog.dismissLoading();
+    });
+  }
+
+  toSummary(item) {
+    if (item.refPk) {
+      startPage(PageList.summary, { action: 'view', chargingPk: item.refPk });
+    }
+  }
+
+  getTransTitle(item) {
+    if (item.amountSymbol == 'M') {
+      return "Charging Session";
+    } else {
+      return "Purchase Credits";
+    }
+  }
+
+  listItem = ({item, index, separators}) => {
+    return (
+      <Pressable
+        android_ripple={ripple}
+        style={styles.itemView}
+        onPress={() => {
+          this.toSummary(item)
+        }}>
+        {/* <Image
+          style={styles.iconType}
+          resizeMode="contain"
+          source={(item.amountSymbol == 'P' || item.remarks) ? IconPayment : IconCharge}/> */}
+        <View style={styles.itemContent}>
+          <View style={ui.flex1}>
+            <TextView style={styles.issueName}>{this.getTransTitle(item)}</TextView>
+            <TextView style={styles.issueDesc}>{item.createTime}</TextView>
+            { item.amountSymbol == 'M'
+            ? <TextView style={styles.issueDesc}>{item.siteName || "Charging"}</TextView>
+            : <TextView style={styles.issueDesc}>{$t('wallet.topUp') + ", " + item.amount}</TextView>
+            }
+            <TextView style={styles.issueDesc}>{$t('wallet.labelTransactionId') + item.creditRecordPk}</TextView>
+          </View>
+          { item.amountSymbol == 'M'
+            ? <TextView style={styles.amountDuct}>- {item.amount}</TextView>
+            : <TextView style={styles.amountText}>+ {item.amount}</TextView>
+          }
+        </View>
+      </Pressable>
+    )
+  }
+
+  divideView = (props) => {
+    return (<View style={styles.divide}></View>)
+  }
+
+  bottomView = () => {
+    if (!this.state.hasMore) {
+      return (<Text style={styles.noMore}>{$t('wallet.noMore')}</Text>)
+    } else {
+      return null
+    }
+  }
+
+  render() {
+    return (
+      <FlatList
+        style={styles.listView}
+        data={this.state.dataList}
+        renderItem={this.listItem}
+        ItemSeparatorComponent={this.divideView}
+        ListFooterComponent={this.bottomView}
+        keyExtractor={item => item.creditRecordPk}
+        onEndReached={() => this.getNextPage()}
+        onEndReachedThreshold={0.3}
+        refreshControl={
+          <RefreshControl
+            {...MyRefreshProps()}
+            refreshing={this.state.refreshing}
+            onRefresh={() => this.onRefresh()}
+          />
+        }
+        ListEmptyComponent={<Text style={styles.noData}>{$t('wallet.noHistoryData')}</Text>}
+      />
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  listView: {
+    flex: 1
+  },
+  itemView: {
+    paddingLeft: 16,
+    paddingRight: 16,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  itemContent: {
+    flex: 1,
+    //marginLeft: 16,
+    ...$padding(16, 4),
+    alignItems: 'flex-start',
+    flexDirection: 'row'
+  },
+  divide: {
+    marginLeft: 16,
+    marginRight: 16,
+    borderTopWidth: 1,
+    borderTopColor: '#eee',
+  },
+  iconType: {
+    width: 28,
+    height: 28
+  },
+  issueName: {
+    color: textPrimary,
+    fontSize: 14,
+    paddingBottom: 2
+  },
+  issueDesc: {
+    color: textSecondary,
+    fontSize: 12
+  },
+  amountDuct: {
+    color: '#FF2E00',
+    fontSize: 14,
+    paddingLeft: 8
+  },
+  amountText: {
+    color: colorPrimary,
+    fontSize: 14,
+    paddingLeft: 8
+  },
+  noData: {
+    color: textPlacehoder,
+    fontSize: 14,
+    padding: 20,
+    textAlign: 'center'
+  },
+  noMore: {
+    color: textPlacehoder,
+    fontSize: 14,
+    padding: 16,
+    textAlign: 'center'
+  }
+})

+ 196 - 0
Strides-APP/app/pages/transaction/PointsHistory.js

@@ -0,0 +1,196 @@
+/**
+ * 积分历史列表页面
+ * @邠心vbe on 2024/05/07
+ */
+import React, { Component } from 'react';
+import { View, Text, RefreshControl, FlatList, StyleSheet } from 'react-native';
+import { MyRefreshProps } from '../../components/ThemesConfig';
+import TextView from '../../components/TextView';
+import Dialog from '../../components/Dialog';
+import apiVoucher from '../../api/apiVoucher';
+import app from '../../../app.json';
+
+export default class PointsHistory extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      dataList: [],
+      hasMore: true,
+      refreshing: false
+    };
+  }
+
+  componentDidMount() {
+    Dialog.showProgressDialog();
+    this.getHistoryList();
+  }
+
+  onRefresh() {
+    this.setState({
+      refreshing: true
+    })
+    this.getHistoryList();
+  }
+
+  getNextPage() {
+    if (this.state.dataList.length > 0 && this.state.hasMore) {
+      console.log("[Points History]", "getNextPage");
+      const last = this.state.dataList[this.state.dataList.length-1]
+      this.getHistoryList(last.pointsHistoryId);
+    }
+  }
+
+  getHistoryList(lastPk="") {
+    apiVoucher.getPointsHistory(lastPk).then(res => {
+      if (res.data) {
+        if (lastPk) {
+          if (res.data.length > 0) {
+            const list = this.state.dataList;
+            this.setState({
+              dataList: list.concat(res.data)
+            });
+          } else {
+            this.setState({
+              hasMore: false
+            })
+          }
+        } else {
+          this.setState({
+            dataList: res.data,
+            hasMore: true
+          });
+        }
+      } else {
+        this.setState({
+          dataList: []
+        });
+      }
+    }).catch(err => {
+      toastShort(err)
+    }).finally(() => {
+      this.setState({
+        refreshing: false
+      });
+      Dialog.dismissLoading();
+    });
+  }
+
+  listItem = ({item, index, separators}) => {
+    return (
+      <View style={styles.itemView}>
+        <View style={styles.itemContent}>
+          <View style={ui.flex1}>
+            <TextView style={styles.issueName}>{item.remarks}</TextView>
+            <TextView style={styles.issueDesc}>{item.dateTime}</TextView>
+            <TextView style={styles.issueDesc}>{(item.siteName ?? item.voucherName) + (item.chargeBoxId ? ", " + item.chargeBoxId : "")}</TextView>
+            <TextView style={styles.issueDesc}>{"Transaction ID: " + item.refId}</TextView>
+          </View>
+          { item.action == "Increase"
+            ? <TextView style={styles.amountText}>+ {item.points}</TextView>
+            : <TextView style={styles.amountDuct}>- {item.points}</TextView>
+          }
+        </View>
+      </View>
+    )
+  }
+
+  divideView = (props) => {
+    return (<View style={styles.divide}></View>)
+  }
+
+  bottomView = () => {
+    if (!this.state.hasMore) {
+      return (<Text style={styles.noMore}>{$t('wallet.noMore')}</Text>)
+    } else {
+      return null
+    }
+  }
+
+  render() {
+    return (
+      <FlatList
+        style={styles.listView}
+        data={this.state.dataList}
+        renderItem={this.listItem}
+        ItemSeparatorComponent={this.divideView}
+        ListFooterComponent={this.bottomView}
+        keyExtractor={item => item.pointsHistoryId}
+        onEndReached={() => this.getNextPage()}
+        onEndReachedThreshold={0.3}
+        refreshControl={
+          <RefreshControl
+            {...MyRefreshProps()}
+            refreshing={this.state.refreshing}
+            onRefresh={() => this.onRefresh()}
+          />
+        }
+        ListEmptyComponent={<Text style={styles.noData}>{$t('points.noData')}</Text>}
+      />
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  listView: {
+    flex: 1
+  },
+  itemView: {
+    paddingLeft: 16,
+    paddingRight: 16,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  itemContent: {
+    flex: 1,
+    //marginLeft: 8,
+    ...$padding(16, 4),
+    alignItems: 'flex-start',
+    flexDirection: 'row'
+  },
+  divide: {
+    marginLeft: 16,
+    marginRight: 16,
+    borderTopWidth: 1,
+    borderTopColor: '#eee',
+  },
+  iconType: {
+    width: 28,
+    height: 28,
+    textAlign: 'center',
+    lineHeight: 27,
+    borderWidth: 1,
+    borderRadius: 30,
+    borderColor: textPrimary
+  },
+  issueName: {
+    color: textPrimary,
+    fontSize: 14,
+    paddingBottom: 2
+  },
+  issueDesc: {
+    color: textSecondary,
+    fontSize: 12
+  },
+  amountDuct: {
+    color: '#FF2E00',
+    fontSize: 14,
+    paddingLeft: 8
+  },
+  amountText: {
+    color: colorPrimary,
+    fontSize: 14,
+    paddingLeft: 8
+  },
+  noData: {
+    color: textPlacehoder,
+    fontSize: 12,
+    padding: 20,
+    textAlign: 'center'
+  },
+  noMore: {
+    color: textPlacehoder,
+    fontSize: 12,
+    padding: 16,
+    textAlign: 'center'
+  }
+})

+ 4 - 5
Strides-APP/app/pages/wallet/Transaction.js → Strides-APP/app/pages/transaction/Transaction.js

@@ -3,13 +3,12 @@
  * @邠心vbe on 2024/05/16
  */
 import React, { Component, useState } from 'react';
-import { View, Text, StyleSheet, RefreshControl } from 'react-native';
-import PointsHistory from '../vouchers/PointsHistory';
+import { ScrollView, StyleSheet, RefreshControl } from 'react-native';
+import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
 import HistoryList from './HistoryList';
-import OverviewV2 from './OverviewV2';
+import OverviewV2 from '../wallet/OverviewV2';
+import PointsHistory from './PointsHistory';
 import app from '../../../app.json';
-import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
-import { ScrollView } from 'react-native';
 import { MyRefreshProps } from '../../components/ThemesConfig';
 
 const TabOverView = () => {

+ 7 - 3
Strides-APP/app/pages/vouchers/ListPoints.js

@@ -11,6 +11,7 @@ import TextView from '../../components/TextView';
 import Button from '../../components/Button';
 import Dialog from '../../components/Dialog';
 import VoucherType from './VoucherType';
+import app from '../../../app.json';
 
 export default class ListPoints extends Component {
   constructor(props) {
@@ -158,9 +159,11 @@ export default class ListPoints extends Component {
           <TextView style={styles.voucherTitle}>{item.voucherName}</TextView>
           <TextView style={styles.voucherDesc}>{item.voucherDesc}</TextView>
           <TextView style={styles.expireDate}>{$t("voucher.expiresOn") + item.expiresOn}</TextView>
-          <View style={styles.rightDash}></View>
-          <View style={styles.topTikDot}></View>
-          <View style={styles.bottomTikDot}></View>
+          { !app.isLumiWhitelabel && <>
+            <View style={styles.rightDash}></View>
+            <View style={styles.topTikDot}></View>
+            <View style={styles.bottomTikDot}></View>
+          </> }
         </View>
         { item.purchasePoints > 0
         ? <Button
@@ -350,6 +353,7 @@ const styles = StyleSheet.create({
     color: textPlacehoder,
     fontSize: 14,
     padding: 16,
+    marginBottom: 16,
     textAlign: 'center'
   }
 })

+ 7 - 3
Strides-APP/app/pages/vouchers/ListVoucher.js

@@ -10,6 +10,7 @@ import ViewRedeem from './ViewRedeem';
 import VoucherType from './VoucherType';
 import apiVoucher from '../../api/apiVoucher';
 import { PageList } from '../Router';
+import app from '../../../app.json';
 
 export default class ListVoucher extends Component {
   constructor(props) {
@@ -125,9 +126,11 @@ export default class ListVoucher extends Component {
           <TextView style={styles.voucherTitle}>{item.voucherName}</TextView>
           <TextView style={styles.voucherDesc}>{item.voucherDesc}</TextView>
           <TextView style={styles.expireDate}>{$t("voucher.expiresOn") + item.expiresOn}</TextView>
-          <View style={styles.rightDash}></View>
-          <View style={styles.topTikDot}></View>
-          <View style={styles.bottomTikDot}></View>
+          { !app.isLumiWhitelabel && <>
+            <View style={styles.rightDash}></View>
+            <View style={styles.topTikDot}></View>
+            <View style={styles.bottomTikDot}></View>
+          </> }
         </View>
         <TextView
           style={[
@@ -287,6 +290,7 @@ const styles = StyleSheet.create({
     color: textPlacehoder,
     fontSize: 14,
     padding: 16,
+    marginBottom: 16,
     textAlign: 'center'
   }
 })

+ 6 - 3
Strides-APP/app/pages/vouchers/VoucherSelect.js

@@ -7,6 +7,7 @@ import TextView from '../../components/TextView';
 import BadgeSelectItem from '../../components/BadgeSelectItem';
 import Button from '../../components/Button';
 import PagerUtil from '../chargeV2/PagerUtil';
+import app from '../../../app.json';
 
 export default class VoucherSelect extends Component {
   constructor(props) {
@@ -143,9 +144,11 @@ export default class VoucherSelect extends Component {
           <TextView style={styles.voucherTitle}>{item.voucherName}</TextView>
           <TextView style={styles.voucherDesc}>{item.voucherDesc}</TextView>
           <TextView style={styles.expireDate}>{$t("voucher.expiresOn") + item.expiresOn}</TextView>
-          <View style={styles.rightDash}></View>
-          <View style={[styles.topTikDot, checked ? styles.activeBorder : {}]}></View>
-          <View style={[styles.bottomTikDot, checked ? styles.activeBorder : {}]}></View>
+          { !app.isLumiWhitelabel && <>
+            <View style={styles.rightDash}></View>
+            <View style={[styles.topTikDot, checked ? styles.activeBorder : {}]}></View>
+            <View style={[styles.bottomTikDot, checked ? styles.activeBorder : {}]}></View>
+          </> }
         </View>
         <TextView
           style={[

+ 1 - 1
Strides-APP/ios/Podfile

@@ -132,7 +132,7 @@ target 'Strides' do
     installer.pods_project.targets.each do |target|
       target.build_configurations.each do |config|
         config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
-        config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = "16.4"
+        #config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = "16.4"
       end
     end
     __apply_Xcode_12_5_M1_post_install_workaround(installer)

+ 11 - 12
Strides-APP/package.json

@@ -43,7 +43,6 @@
     "react-native-camera": "4.2.1",
     "react-native-device-info": "10.9.0",
     "react-native-gesture-handler": "2.12.1",
-    "react-native-gradients": "2.0.1",
     "react-native-i18n": "https://gitee.com/vbes/react-native-i18n.git",
     "react-native-image-crop-picker": "0.40.0",
     "react-native-location-enabler": "https://gitee.com/vbes/react-native-location-enabler.git",
@@ -74,17 +73,17 @@
     "victory-native": "36.6.11"
   },
   "devDependencies": {
-    "@babel/core": "7.22.11",
-    "@babel/preset-env": "7.22.14",
-    "@babel/runtime": "7.22.11",
-    "@react-native/eslint-config": "0.72.2",
-    "@react-native/metro-config": "0.72.11",
-    "babel-jest": "29.6.4",
-    "eslint": "8.19.0",
-    "jest": "29.6.4",
-    "metro-react-native-babel-preset": "0.76.8",
-    "patch-package": "8.0.0",
-    "postinstall-postinstall": "2.1.0",
+    "@babel/core": "^7.20.0",
+    "@babel/preset-env": "^7.20.0",
+    "@babel/runtime": "^7.20.0",
+    "@react-native/eslint-config": "^0.72.2",
+    "@react-native/metro-config": "^0.72.11",
+    "babel-jest": "^29.2.1",
+    "eslint": "^8.19.0",
+    "jest": "^29.2.1",
+    "metro-react-native-babel-preset": "^0.76.8",
+    "patch-package": "^8.0.0",
+    "postinstall-postinstall": "^2.1.0",
     "react-test-renderer": "18.2.0",
     "typescript": "4.8.4"
   },