Explorar el Código

自定义安装条 手机绑定

zhejiushizhuce hace 2 años
padre
commit
aff94b85dc

+ 1 - 0
package.json

@@ -85,6 +85,7 @@
     "styled-theming": "^2.2.0",
     "stylis-plugin-rtl": "^2.1.1",
     "swiper": "^11.1.4",
+    "ua-parser-js": "^1.0.38",
     "use-gauge": "^0.5.0",
     "use-mobile-detect-hook": "^1.0.5"
   },

+ 11 - 3
src/App.js

@@ -76,6 +76,7 @@ import useGlobalClick from "@hooks/useGlobalClick";
 import SoundHelp from "@utils/SoundManger";
 import {ROUTE_PATH} from "@constants/routes";
 import {onChangeUser, onChangeUserID} from "@features/actions/mainAction";
+import {isInAppBrowser, isShowInstallBox, openUrlUseChrome} from "@utils/UAUtils";
 const App = () => {
     const dispatch = useDispatch();
     const fetchStatus=useSelector(getFetchStatus);
@@ -290,10 +291,17 @@ const App = () => {
             if (global.installed===1)return;
             global.installed=1;
             let time=promoteInstall*1000;
+            global.showBindPhone();
+            // time=1000;
             setTimeout(async ()=>{
-                if (window.progressier==null)return ;
-                if (window.progressier.detection.isStandalone())return;
-                window.progressier.flow.init("install",true)
+                // if (window.progressier==null)return ;
+                // if (window.progressier.detection.isStandalone())return;
+                // window.progressier.flow.init("install",true)
+                if (!isShowInstallBox())return;
+                if (isInAppBrowser()){
+                    openUrlUseChrome();
+                }
+                global.showInstallBox();
             },time);
         }
     },[promoteInstall])

+ 12 - 0
src/Global.js

@@ -64,4 +64,16 @@ global.showMailNew=()=>{
 global.hideMailNew=()=>{
     if (global.mailNew)global.mailNew.hide();
 }
+global.showInstallBox=()=>{
+    if (global.installBox)global.installBox.show();
+}
+global.hideInstallBox=()=>{
+    if (global.installBox)global.installBox.hide();
+}
+global.showBindPhone=()=>{
+    if (global.bindPhone)global.bindPhone.show();
+}
+global.hideBindPhone=()=>{
+    if (global.bindPhone)global.bindPhone.hide();
+}
 //wd pass

+ 4 - 0
src/api/request.js

@@ -6,6 +6,7 @@ import {getHttpMsg, localStorageGet} from "@utils/helpers";
 import {LOCAL_ACTIVITY_C, LOCAL_SIGN} from "@constants/localkey";
 import {toast} from "react-toastify";
 import LZString, {compress, compressToEncodedURIComponent, compressToUTF16, decompressFromBase64} from "lz-string";
+import {isStandalone, isTWA} from "@utils/UAUtils";
 
 // axios.defaults.withCredentials = true
 const getSign=()=>{
@@ -25,6 +26,9 @@ const getComParams=()=>{
     if (!!c)params.c=c;
     params._d=global.device;
     params._t=Date.now()+"_"+global.width+"_"+global.height;
+    if (isStandalone()||isTWA()){
+        params.pwa=1;
+    }
     return params;
 }
 const checkResponse=(data)=>{

+ 4 - 0
src/layout/Layout.jsx

@@ -30,6 +30,8 @@ import DropCoins from "@components/DropCoins";
 import {toast} from "react-toastify";
 import useEdgeGesture from "@hooks/useEdgeGesture";
 import MailNew from "@components/MailNew";
+import WInstallBox from "@widgets/WInstallBox";
+import WBindPhone from "@widgets/WBindPhone";
 const PageNotFound = lazy(() => import('@pages/PageNotFound'));
 
 const Layout=({...props})=>{
@@ -120,6 +122,8 @@ const Layout=({...props})=>{
                     setIsShowWheel(false);
                     if (props&&props.onCloseHallWheel)props.onCloseHallWheel()
                 }}></WHallWheel>
+                <WInstallBox/>
+                <WBindPhone/>
             </>
         )
     }

+ 18 - 1
src/locales/languages/en_US.json

@@ -108,7 +108,13 @@
       "empty": "Please enter password!"
     },
     "success": "Login successful, welcome back!",
-    "notfound": "Your account was not found, please register or try again!"
+    "notfound": "Your account was not found, please register or try again!",
+    "login_type": {
+      "password": "Password",
+      "otp": "OTP"
+    },
+    "otp_ph": "Verification Code",
+    "guest_login": "Guest Login"
   },
   "logout": {
     "success": "You have exited successfully."
@@ -597,6 +603,17 @@
     "reconnect": "Reconnect",
     "disconnected": "The network is disconnected. Do you want to reconnect?"
   },
+  "installBox": {
+    "tips": "We would love to add 24680.com to your home screen for the latest free bets and bonus updates.",
+    "btn_cancel": "Cancel",
+    "btn_install": "Install"
+  },
+  "bindPhone": {
+    "title_link_phone": "Account Upgrade",
+    "phone_award": "Award:",
+    "btn_submit": "SUBMIT",
+    "tips": "Link your Mobile and get 5 chips reward"
+  },
   "web": {
     "activity": {
       "redpack_not_available": "There is no RedPack",

+ 5 - 0
src/locales/languages/es_MX.json

@@ -586,6 +586,11 @@
     "reconnect": "Reconectar",
     "disconnected": "La red se ha desconectado. ¿Quieres volver a conectarte?"
   },
+  "installBox": {
+    "tips": "Nos encantaría añadir 24680.com a tu pantalla de inicio para las últimas apuestas gratuitas y actualizaciones de bonos.",
+    "btn_cancel": "Cancelar",
+    "btn_install": "Instalar"
+  },
   "web": {
     "activity": {
       "redpack_not_available": "No hay RedPack",

+ 5 - 0
src/locales/languages/pt_BR.json

@@ -586,6 +586,11 @@
     "reconnect": "Reconectar",
     "disconnected": "A rede foi desconectada. Deseja reconectar?"
   },
+  "installBox": {
+    "tips": "Adoraríamos adicionar 24680.com à sua tela inicial para as últimas apostas gratuitas e atualizações de bônus.",
+    "btn_cancel": "Cancelar",
+    "btn_install": "Instalar"
+  },
   "web": {
     "activity": {
       "redpack_not_available": "Não há RedPack",

+ 5 - 0
src/locales/languages/ru_RU.json

@@ -586,6 +586,11 @@
     "reconnect": "переподключиться",
     "disconnected": "Сеть отключена. Хотите восстановить соединение?"
   },
+  "installBox": {
+    "tips": "Мы бы хотели добавить 24680.com на ваш рабочий стол для последних бесплатных ставок и обновлений бонусов.",
+    "btn_cancel": "Отмена",
+    "btn_install": "Установить"
+  },
   "web": {
     "activity": {
       "redpack_not_available": "Нет RedPack",

+ 5 - 0
src/locales/languages/zh_TW.json

@@ -586,6 +586,11 @@
     "reconnect": "重新連接",
     "disconnected": "網路已斷開連接,要重新連接嗎?"
   },
+  "installBox": {
+    "tips": "我們希望將 24680.com 添加到您的主屏幕,以獲取最新的免費投注和獎金更新。",
+    "btn_cancel": "取消",
+    "btn_install": "安裝"
+  },
   "web": {
     "activity": {
       "redpack_not_available": "沒有紅包",

+ 1 - 0
src/styles/_vars.scss

@@ -69,6 +69,7 @@ $width-desktop-big-max:1564px;
     --zindex-drawer:1200;
     --zindex-popup:1300;
     --zindex-effect:1400;
+    --zindex-install:1500;
     --coin-yellow:#edc349;
     //--progressierBg
     --progressierBg:var(--widget) !important;

+ 265 - 0
src/utils/UAUtils.js

@@ -0,0 +1,265 @@
+import { UAParser } from 'ua-parser-js';
+// Constants for browsers
+const isAndroid=()=>{
+    try {
+        return window.progressier.detection.isAndroid();
+    } catch (t) {
+        return false
+    }
+}
+const isIOS=()=>{
+    try {
+        return window.progressier.detection.isIOS();
+    } catch (t) {
+        return false
+    }
+}
+const isMacOs=()=>{
+    try {
+        return window.progressier.detection.isMacOs();
+    } catch (t) {
+        return false
+    }
+}
+const isWindows=()=>{
+    try {
+        return window.progressier.detection.isWindows();
+    } catch (e) {
+        return false
+    }
+}
+const isSafari=()=>{
+    try {
+        return window.progressier.detection.isSafari();
+    } catch (e) {
+        return false
+    }
+}
+const isFirefox=()=>{
+    try {
+        return window.progressier.detection.isFirefox();
+    } catch (n) {
+        return false
+    }
+}
+const isSamsungInternet=()=>{
+    try {
+        return window.progressier.detection.isSamsungInternet();
+    } catch (e) {
+        return false
+    }
+}
+const isChromeEdgeOnMacOs=()=>{
+    try {
+        return window.progressier.detection.isChromeEdgeOnMacOs();
+    } catch (e) {
+        return false
+    }
+}
+const isArc=()=>{
+    try {
+        return window.progressier.detection.isArc();
+    } catch (e) {
+        return false
+    }
+}
+const isEdge=()=>{
+    try {
+        return window.progressier.detection.isEdge();
+    } catch (e) {
+        return false
+    }
+}
+const isChrome=()=>{
+    try {
+        return window.progressier.detection.isChrome();
+    } catch (r) {
+        return false
+    }
+}
+const isOpera=()=>{
+    try {
+        return window.progressier.detection.isOpera();
+    } catch (e) {
+        return false
+    }
+}
+const supportsNativeiOSPush=()=>{
+    try {
+        return window.progressier.detection.supportsNativeiOSPush();
+    }
+    catch (e){
+        return false;
+    }
+}
+const appstore=()=>{
+    return supportsNativeiOSPush()
+}
+const supportsPush=()=>{
+    try {
+        return window.progressier.detection.supportsPush();
+    }
+    catch (e){
+        return false;
+    }
+}
+const isFromTwitter=()=>{
+    try {
+        return window.progressier.detection.isFromTwitter();
+    }
+    catch (e){
+        return false;
+    }
+}
+const isInAppBrowser=()=>{
+    try {
+        return window.progressier.detection.isInAppBrowser();
+    }
+    catch (e){
+        return false;
+    }
+}
+const isChromeOS=()=>{
+    try {
+        return window.progressier.detection.isChromeOS();
+    } catch (e) {
+        return false
+    }
+}
+const isDesktop=()=>{
+    try {
+        return window.progressier.detection.isDesktop();
+    } catch (e) {
+        return false
+    }
+}
+const isSafariWithPushOnMacOs=()=>{
+   try {
+       return window.progressier.detection.isSafariWithPushOnMacOs();
+   }
+   catch (e){
+       return false
+   }
+}
+const isSafariWithInstallationMacOS=()=>{
+    try {
+        return window.progressier.detection.isSafariWithInstallationMacOS();
+    } catch (a) {
+        return false
+    }
+}
+const canBePushPrompted=()=>{
+    try {
+        return window.progressier.detection.canBePushPrompted();
+    }
+    catch (e){
+        return false
+    }
+}
+const isStandalone=()=>{
+    try {
+        return window.progressier.detection.isStandalone();
+    }
+    catch (e){
+        return false
+    }
+}
+const iosVersion=()=>{
+    try {
+        return window.progressier.detection.iosVersion();
+    }
+    catch (e){
+        return false
+    }
+}
+const recentiOS=()=>{
+    try {
+        return window.progressier.detection.recentiOS();
+    }
+    catch (e){
+        return false
+    }
+}
+const isTWA=()=>{
+    try {
+        return window.progressier.detection.isTWA();
+    }
+    catch (e){
+        return false
+    }
+}
+const requiresInScopeSw=()=>{
+    try {
+        return window.progressier.detection.requiresInScopeSw();
+    }
+    catch (e){
+        return false
+    }
+}
+const isIframe=()=>{
+    try {
+        return window.progressier.detection.isIframe();
+    }
+    catch (e){
+        return false
+    }
+}
+const isShowInstallBox=()=>{
+    if (isStandalone())return false;
+    if (isTWA())return false;
+    return true;
+}
+const openProgressierInstall=()=>{
+    if(window.progressier&&window.progressier.flow)window.progressier.flow.init("install",true)
+}
+const openUrlUseChrome=(url="")=>{
+    try {
+        if (url==null||url==="")url=window.location.href;
+        if (isTWA())return;
+        if (isIOS()){
+            let r = "googlechromes://" + url.replace(/^https?:\/\//, "");
+            window.location.href = r
+        }
+        else if (isAndroid()){
+            const r = url.split(":")[0];
+            let l = url.replace(/^https?:\/\//, "");
+            window.location.href = "intent://".concat(l, "#Intent;scheme=").concat(r, ";package=com.android.chrome;end")
+        }
+    }
+    catch (e){
+        return false;
+    }
+}
+export {
+    isAndroid,
+    isIOS,
+    isMacOs,
+    isWindows,
+    isSafari,
+    isFirefox,
+    isSamsungInternet,
+    isChromeEdgeOnMacOs,
+    isArc,
+    isEdge,
+    isChrome,
+    isOpera,
+    supportsNativeiOSPush,
+    appstore,
+    supportsPush,
+    isFromTwitter,
+    isInAppBrowser,
+    isChromeOS,
+    isDesktop,
+    isSafariWithPushOnMacOs,
+    isSafariWithInstallationMacOS,
+    canBePushPrompted,
+    isStandalone,
+    iosVersion,
+    recentiOS,
+    isTWA,
+    requiresInScopeSw,
+    isIframe,
+    isShowInstallBox,
+    openUrlUseChrome,
+    openProgressierInstall
+};

+ 148 - 62
src/widgets/LoginForm/index.jsx

@@ -33,6 +33,8 @@ const i18n=global.i18n;
 const TYPE_LOGIN="login";
 const TYPE_REGISTER="register";
 
+const LOGIN_TYPE_PASS=0;
+const LOGIN_TYPE_OTP=1;
 
 const LoginForm = ({close}) => {
     const {theme,isMobile} = useThemeProvider();
@@ -44,6 +46,7 @@ const LoginForm = ({close}) => {
     const regStatus=useSelector(getRegStatus);
     const dispatch = useDispatch();
     const navigate=useNavigate();
+    const [loginType,setLoginType]=useState(LOGIN_TYPE_PASS);
     const {register, handleSubmit, formState: {errors}, control} = useForm({
         defaultValues: {
             // country:'55',
@@ -51,7 +54,8 @@ const LoginForm = ({close}) => {
             password: '',
             email:"",
             repassword:"",
-            lang:""
+            lang:"",
+            code:""
         }
     });
     const {pathname,hash} = useLocation();
@@ -107,68 +111,150 @@ const LoginForm = ({close}) => {
             </div>
         )
     }
-    const renderLoginForm=()=>{
+    const renderLoginToReg=()=>{
         return (
-            <Spring type="slideUp" >
+            <div className={styles.l_reg_new}>
+                <div>{i18n.t("login.reg_new_des")}</div>
+                <div className={styles.reg_btn} onClick={() => {
+                    // setShowType(TYPE_REGISTER);
+                    if (isMobile()) navigatRoute(ROUTE_PATH.SIGN_UP)
+                    else setShowType(TYPE_REGISTER);
+                }}>
+                    {i18n.t("sign_up")}
+                </div>
+            </div>
+        )
+    }
+    const renderGuestLogin=()=>{
+        return (
+            <button type={"button"} className={styles.guest_btn}>
+                {i18n.t("login.guest_login")}
+            </button>
+        )
+    }
+    const renderPassLogin = () => {
+        return (
+            <form autoComplete="off" className={styles.l_form} onSubmit={handleSubmit((data) => {
+                onSubmitLogin(data);
+            })}>
+                <Controller control={control}
+                            name="account"
+                            rules={{required: true}}
+                            render={({field: {ref, onChange, value}, fieldState: {error}}) => {
+                                let message = error ? i18n.t("form_errors.phone_" + country) : "";
+                                return (<PhoneInput
+                                    className={classNames({'input-control--error': error})}
+                                    value={value}
+                                    message={message}
+                                    onChange={e => onChange(e.target.value)}
+                                    onChangeCountry={(country) => {
+                                        setCountry(country);
+                                    }}
+                                    placeholder={i18n.t('login.phone_ph')}
+                                    innerRef={ref}/>)
+                            }}
+                />
+                <Controller control={control}
+                            name="password"
+                            rules={{required: true, minLength: 6}}
+                            render={({field: {ref, onChange, value}, fieldState: {error}}) => (
+                                <PasswordInput
+                                    className={classNames({'input-control--error': error})}
+                                    value={value}
+                                    message={error ? i18n.t("form_errors.pass") : ''}
+                                    onChange={e => onChange(e.target.value)}
+                                    placeholder={i18n.t('login.login_pwd')}
+                                    innerRef={ref}/>
+                            )}
+                />
+                <button className={"button-green"}>
+                    {
+                        loginStatus === REQ_STATUS.LOADING ? renderLoading() : i18n.t("login.sign_in_btn")
+                    }
+                </button>
+                {renderGuestLogin()}
+                {renderLoginToReg()}
+            </form>
+        )
+    }
+    const renderOTPLogin=()=>{
+        return (
+            <form autoComplete="off" className={styles.l_form} onSubmit={handleSubmit((data) => {
+                onSubmitLogin(data);
+            })}>
+                <Controller control={control}
+                            name="account"
+                            rules={{required: true}}
+                            render={({field: {ref, onChange, value}, fieldState: {error}}) => {
+                                let message = error ? i18n.t("form_errors.phone_" + country) : "";
+                                return (<PhoneInput
+                                    className={classNames({'input-control--error': error})}
+                                    value={value}
+                                    message={message}
+                                    onChange={e => onChange(e.target.value)}
+                                    onChangeCountry={(country) => {
+                                        setCountry(country);
+                                    }}
+                                    placeholder={i18n.t('login.phone_ph')}
+                                    innerRef={ref}/>)
+                            }}
+                />
+                <Controller control={control}
+                            name="code"
+                            rules={{required: true}}
+                            render={({field: {ref, onChange, value}, fieldState: {error}}) => {
+                                return (
+                                    <div className={`d-flex flex-column g-6`}>
+                                        <div
+                                            className={`input-control ${classNames({'input-control--error': error})}`}>
+                                            <input
+                                                type={'text'}
+                                                autoComplete="off"
+                                                ref={ref}
+                                                onChange={e => onChange(e.target.value)}
+                                                placeholder={i18n.t('login.otp_ph')}
+                                            />
+                                            <button type={"button"} className={styles.otp_btn}>OTP</button>
+                                        </div>
+                                    </div>
+                                )
+                            }}
+                />
+                <button className={"button-green"}>
+                    {
+                        loginStatus === REQ_STATUS.LOADING ? renderLoading() : i18n.t("login.sign_in_btn")
+                    }
+                </button>
+                {renderGuestLogin()}
+                {renderLoginToReg()}
+            </form>
+        )
+    }
+    const renderLoginForm = () => {
+        return (
+            <Spring type="slideUp">
                 <div className={styles.l_title}>{i18n.t("login.login_title")}</div>
-                <form autoComplete="off" className={styles.l_form} onSubmit={handleSubmit((data) => {
-                    onSubmitLogin(data);
-                })}>
-                    <Controller control={control}
-                                name="account"
-                                rules={{required: true}}
-                                render={({field: {ref, onChange, value}, fieldState: {error}}) => {
-                                    let message=error?i18n.t("form_errors.phone_"+country):"";
-                                    return (<PhoneInput
-                                        className={classNames({'input-control--error': error})}
-                                        value={value}
-                                        message={message}
-                                        onChange={e => onChange(e.target.value)}
-                                        onChangeCountry={(country)=>{
-                                            setCountry(country);
-                                        }}
-                                        placeholder={i18n.t('login.phone_ph')}
-                                        innerRef={ref}/>)
-                                }}
-                    />
-                    <Controller control={control}
-                                name="password"
-                                rules={{required: true,minLength:6}}
-                                render={({field: {ref, onChange, value}, fieldState: {error}}) => (
-                                    <PasswordInput
-                                        className={classNames({'input-control--error': error})}
-                                        value={value}
-                                        message={error?i18n.t("form_errors.pass"):''}
-                                        onChange={e => onChange(e.target.value)}
-                                        placeholder={i18n.t('login.login_pwd')}
-                                        innerRef={ref}/>
-                                )}
-                    />
-                    {/*<div className={styles.l_forgot}>*/}
-                    {/*    <button className={styles.forgot_btn}>*/}
-                    {/*        {i18n.t("login.forgot")}*/}
-                    {/*    </button>*/}
-                    {/*</div>*/}
-                    <button className={"button-green"}>
-                        {
-                            loginStatus===REQ_STATUS.LOADING?renderLoading():i18n.t("login.sign_in_btn")
-                        }
+                <div className={styles.l_type_tabs}>
+                    <button className={`${styles.l_type_btn} ${loginType === LOGIN_TYPE_PASS ? styles.active : ''}`}
+                            onClick={() => {
+                                setLoginType(LOGIN_TYPE_PASS)
+                            }}>
+                        <span>{i18n.t("login.login_type.password")}</span>
                     </button>
-                    <div className={styles.l_reg_new}>
-                        <div>{i18n.t("login.reg_new_des")}</div>
-                        <div className={styles.reg_btn} onClick={()=>{
-                            // setShowType(TYPE_REGISTER);
-                            if(isMobile())navigatRoute(ROUTE_PATH.SIGN_UP)
-                            else setShowType(TYPE_REGISTER);
-                        }}>
-                            {i18n.t("sign_up")}
-                        </div>
-                    </div>
-                </form>
+                    <button className={`${styles.l_type_btn} ${loginType === LOGIN_TYPE_OTP ? styles.active : ''}`}
+                            onClick={() => {
+                                setLoginType(LOGIN_TYPE_OTP)
+                            }}>
+                        <span>{i18n.t("login.login_type.otp")}</span>
+                    </button>
+                </div>
+                {
+                    loginType===LOGIN_TYPE_PASS?renderPassLogin():renderOTPLogin()
+                }
             </Spring>
         )
     }
-    const renderRegisterForm=()=>{
+    const renderRegisterForm = () => {
         return (
             <Spring type="slideUp">
                 <div className={styles.l_title}>{i18n.t("login.reg_title")}</div>
@@ -177,15 +263,15 @@ const LoginForm = ({close}) => {
                 })}>
                     <Controller control={control}
                                 name="account"
-                                rules={{required: true,validate:validatePhone}}
+                                rules={{required: true, validate: validatePhone}}
                                 render={({field: {ref, onChange, value}, fieldState: {error}}) => {
-                                    let message=error?i18n.t("form_errors.phone_"+country):"";
+                                    let message = error ? i18n.t("form_errors.phone_" + country) : "";
                                     return (<PhoneInput
                                         className={classNames({'input-control--error': error})}
                                         value={value}
                                         message={message}
                                         onChange={e => onChange(e.target.value)}
-                                        onChangeCountry={(country)=>{
+                                        onChangeCountry={(country) => {
                                             setCountry(country);
                                         }}
                                         placeholder={i18n.t('login.phone_ph')}
@@ -194,12 +280,12 @@ const LoginForm = ({close}) => {
                     />
                     <Controller control={control}
                                 name="password"
-                                rules={{required: true,minLength:6}}
+                                rules={{required: true, minLength: 6}}
                                 render={({field: {ref, onChange, value}, fieldState: {error}}) => (
                                     <PasswordInput
                                         className={classNames({'input-control--error': error})}
                                         value={value}
-                                        message={error?i18n.t("form_errors.pass"):''}
+                                        message={error ? i18n.t("form_errors.pass") : ''}
                                         onChange={e => onChange(e.target.value)}
                                         placeholder={i18n.t('login.login_pwd')}
                                         innerRef={ref}/>

+ 57 - 0
src/widgets/LoginForm/styles.module.scss

@@ -65,3 +65,60 @@
     color: var(--primary-color);
   }
 }
+.l_type_tabs{
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 1rem;
+  & .l_type_btn{
+    cursor: pointer;
+    width: 50%;
+    height: 2.75rem;
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    span{
+      position: relative;
+      z-index: 2;
+      font-size: 1rem;
+      line-height: 1.25rem;
+      color: var(--text);
+    }
+    &.active{
+      &:after{
+        content: " ";
+        width: 100%;
+        height: 1.375rem;
+        border-bottom: 1px solid var(--primary-color);
+        background: linear-gradient(180deg, rgba(59, 193, 23, 0) 0%, rgba(59, 193, 23, 0) .01%, rgba(59, 193, 23, .2) 100%);
+        position: absolute;
+        left: 0;
+        bottom: 0;
+      }
+      span{
+        color: var(--header);
+        font-weight: 600;
+      }
+    }
+  }
+}
+.otp_btn{
+  width: 3rem;
+  height: 2.5rem;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: flex-end;
+  color: var(--primary-color);
+  text-align: right;
+  font-size: 1rem;
+}
+.guest_btn{
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  color: var(--text);
+}

+ 133 - 0
src/widgets/WBindPhone/index.jsx

@@ -0,0 +1,133 @@
+import React, {useEffect, useState} from "react";
+import Popup from "@components/Popup";
+import styles from "./styles.module.scss";
+import usdimg from "@assets/lvicon/USD.black.png";
+import {Controller, useForm} from "react-hook-form";
+import PhoneInput from "@components/UInput/PhoneInput";
+import classNames from "classnames";
+import PasswordInput from "@components/UInput/PasswordInput";
+import {validateCountryPhone} from "@utils/helpers";
+import {REQ_STATUS} from "@api/config";
+const i18n=global.i18n;
+const WBindPhone=({})=>{
+    const [isShow, setIsShow] = useState(false);
+    const regCoins=5;
+    const [country, setCountry] = useState("55");
+    const {register, handleSubmit, formState: {errors}, control} = useForm({
+        defaultValues: {
+            phone: '',
+            password: '',
+            repassword:'',
+            code:''
+        }
+    });
+    // /bindsms    {phone,code,password,repassword}
+    const show=()=>{
+        setIsShow(true);
+    }
+    const hide=()=>{
+        setIsShow(false);
+    }
+    const validatePhone=(value)=>{
+        return validateCountryPhone(country,value);
+    }
+    const onSubmitBind=(data)=>{
+
+    }
+    useEffect(()=>{
+        const bindPhone={
+            show,
+            hide
+        };
+        global.bindPhone=bindPhone;
+    },[])
+    return (
+       <Popup onClose={hide} open={isShow}>
+           <div className={styles.container}>
+                <span className={styles.title}>
+                    {i18n.t("bindPhone.title_link_phone")}
+                </span>
+               <div className={styles.award_con}>
+                   {i18n.t("bindPhone.phone_award")}
+                   <img src={usdimg}/>
+                   {regCoins}
+               </div>
+               <form autoComplete="off" className={styles.l_form} onSubmit={handleSubmit((data) => {
+                   onSubmitBind(data);
+               })}>
+                   <Controller control={control}
+                               name="phone"
+                               rules={{required: true, validate: validatePhone}}
+                               render={({field: {ref, onChange, value}, fieldState: {error}}) => {
+                                   let message = error ? i18n.t("form_errors.phone_" + country) : "";
+                                   return (<PhoneInput
+                                       className={classNames({'input-control--error': error})}
+                                       value={value}
+                                       message={message}
+                                       onChange={e => onChange(e.target.value)}
+                                       onChangeCountry={(country) => {
+                                           setCountry(country);
+                                       }}
+                                       placeholder={i18n.t('login.phone_ph')}
+                                       innerRef={ref}/>)
+                               }}
+                   />
+                   <Controller control={control}
+                               name="password"
+                               rules={{required: true, minLength: 6}}
+                               render={({field: {ref, onChange, value}, fieldState: {error}}) => (
+                                   <PasswordInput
+                                       className={classNames({'input-control--error': error})}
+                                       value={value}
+                                       message={error ? i18n.t("form_errors.pass") : ''}
+                                       onChange={e => onChange(e.target.value)}
+                                       placeholder={i18n.t('login.login_pwd')}
+                                       innerRef={ref}/>
+                               )}
+                   />
+                   <Controller control={control}
+                               name="repassword"
+                               rules={{required: true, minLength: 6}}
+                               render={({field: {ref, onChange, value}, fieldState: {error}}) => (
+                                   <PasswordInput
+                                       className={classNames({'input-control--error': error})}
+                                       value={value}
+                                       message={error ? i18n.t("form_errors.pass") : ''}
+                                       onChange={e => onChange(e.target.value)}
+                                       placeholder={i18n.t('login.re_pwd')}
+                                       innerRef={ref}/>
+                               )}
+                   />
+                   <Controller control={control}
+                               name="code"
+                               rules={{required: true}}
+                               render={({field: {ref, onChange, value}, fieldState: {error}}) => {
+                                   return (
+                                       <div className={`d-flex flex-column g-6`}>
+                                           <div
+                                               className={`input-control ${classNames({'input-control--error': error})}`}>
+                                               <input
+                                                   type={'text'}
+                                                   autoComplete="off"
+                                                   ref={ref}
+                                                   onChange={e => onChange(e.target.value)}
+                                                   placeholder={i18n.t('login.otp_ph')}
+                                               />
+                                               <button type={"button"} className={styles.otp_btn}>OTP</button>
+                                           </div>
+                                       </div>
+                                   )
+                               }}
+                   />
+                   <button className={"button-green"}>
+                       {
+                           i18n.t("bindPhone.btn_submit")
+                       }
+                   </button>
+                   <span className={styles.tips}>{i18n.t("bindPhone.tips")}</span>
+               </form>
+           </div>
+       </Popup>
+    )
+}
+export default WBindPhone;

+ 52 - 0
src/widgets/WBindPhone/styles.module.scss

@@ -0,0 +1,52 @@
+.container{
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  gap: 0.5rem;
+  pointer-events: auto;
+  & .title{
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: center;
+    color: var(--header);
+    font-weight: 600;
+    font-size: 1rem;
+  }
+  & .award_con{
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: flex-end;
+    color: var(--coin-yellow);
+    gap: 0.25rem;
+    img{
+      width: 1rem;
+      height: 1rem;
+    }
+    font-size: 1rem;
+  }
+  & .l_form{
+    display: flex;
+    flex-direction: column;
+    gap: 1rem;
+    & .otp_btn{
+      width: 3rem;
+      height: 2.5rem;
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: flex-end;
+      color: var(--primary-color);
+      text-align: right;
+      font-size: 1rem;
+    }
+    & .tips{
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: center;
+      color: var(--coin-yellow);
+    }
+  }
+}

+ 120 - 0
src/widgets/WInstallBox/index.jsx

@@ -0,0 +1,120 @@
+import {useEffect, useRef, useState} from "react";
+import {useThemeProvider} from "@contexts/themeContext";
+import MuiDrawer from "@mui/material/Drawer";
+import styles from "./styles.module.scss";
+import {
+    isInAppBrowser,
+    openProgressierInstall,
+    openUrlUseChrome
+} from "@utils/UAUtils";
+import Fade from "@mui/material/Fade";
+import Grow from "@mui/material/Grow";
+import Modal from "@mui/material/Modal";
+const i18n=global.i18n;
+
+const WInstallBox = () => {
+    const {isMobile} = useThemeProvider();
+    const [isShow, setIsShow] = useState(false);
+    const installRef = useRef(null);
+    const [iconUrl, setIconUrl] = useState("");
+    const show=()=>{
+        setIsShow(true);
+    }
+    const hide=()=>{
+        setIsShow(false);
+    }
+    const onInstall=()=>{
+        if (isInAppBrowser()){
+            openUrlUseChrome();
+        }
+        else
+        {
+            openProgressierInstall();
+            setIsShow(false);
+        }
+    }
+    const onCancel=()=>{
+        setIsShow(false);
+    }
+    useEffect(()=>{
+        const installBox={
+            show,
+            hide
+        };
+        global.installBox=installBox;
+        if (window.progressier!=null){
+            if (window.progressier.data!=null){
+                let params=window.progressier.data.params;
+                if (params!=null){
+                    setIconUrl(params.icon180);
+                }
+            }
+        }
+    },[])
+    const renderInstall=()=>{
+        return (
+            <div className={styles.continer}>
+                <div className={styles.install_con}>
+                    <img src={iconUrl}/>
+                    <span>{i18n.t("installBox.tips")}</span>
+                </div>
+                <div className={styles.install_btn_con}>
+                    <button className={styles.install_btn} onClick={onCancel}>
+                        {i18n.t("installBox.btn_cancel")}
+                    </button>
+                    <button className={`${styles.install_btn} ${styles.btn_green}`} onClick={onInstall}>
+                        {i18n.t("installBox.btn_install")}
+                    </button>
+                </div>
+            </div>
+        )
+    }
+    const renderMobile = () => {
+        return (
+            <MuiDrawer open={isShow}
+                       anchor={"bottom"}
+                       sx={{
+                           pointerEvents: 'auto',
+                           zIndex: 'var(--zindex-install)',
+                           "& .MuiDrawer-paper": {
+                               boxSizing: "border-box",
+                               overflow: 'visible !important',
+                               backgroundColor: '#fff;',
+                               boxShadow: 'var(--widget-shadow);',
+                               borderTopLeftRadius: '0.71rem',
+                               borderTopRightRadius: '0.71rem',
+                               padding: '1.42rem'
+                           },
+                       }}
+                       variant="temporary"
+                       onClose={()=>{
+                           // setIsShow(false)
+                       }}>
+                {renderInstall()}
+            </MuiDrawer>
+        )
+    }
+    const renderDesktop = () => {
+        return (
+            <Modal open={isShow} onClose={()=>{
+
+            }} closeAfterTransition>
+                <Fade in={isShow} timeout={500}>
+                    <div className="popup-container">
+                        <Grow in={isShow} timeout={500}>
+                            <div className={styles.pop_con} style={{minHeight: 'unset'}}>
+                                {renderInstall()}
+                            </div>
+                        </Grow>
+                    </div>
+                </Fade>
+            </Modal>
+        )
+    }
+    return (
+        <>
+        {isMobile()?renderMobile():renderDesktop()}
+        </>
+    )
+}
+export default WInstallBox;

+ 49 - 0
src/widgets/WInstallBox/styles.module.scss

@@ -0,0 +1,49 @@
+.continer{
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  & .install_con{
+    display: flex;
+    flex-direction: row;
+    gap: 0.8rem;
+    font-size: 0.99rem;
+    line-height: 1.42rem;
+    color: var(--text-light);
+    img{
+      width: 4.26rem;
+      height: 4.26rem;
+      border-radius: 0.71rem;
+    }
+  }
+  & .install_btn_con{
+    display: flex;
+    flex-direction: row;
+    justify-content: flex-end;
+    padding: 0.25rem;
+    font-size: 0.85rem;
+    gap: 0.25rem;
+    & .install_btn{
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      padding: 0.5rem;
+      color: var(--text-light);
+      border-radius: 0.42rem;
+      min-width: 5.11rem;
+      font-weight: 600;
+    }
+    & .btn_green{
+      border: 2px solid #59c055;
+      color: #59c055;
+    }
+  }
+}
+.pop_con{
+  display: flex;
+  padding: 1.42rem;
+  border-radius: 0.71rem;
+  background-color: #fff;
+  max-width: 25.875rem;
+  pointer-events: auto;
+}