fix: build ứng dụng trên android và có thể đăng nhập bằng Google Oauth
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Send, MessageSquare, User, Loader2, Trash2 } from 'lucide-react';
|
||||
import { io } from 'socket.io-client';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { useTourStore } from '@/store/useTourStore';
|
||||
import { ConfirmModal } from './ConfirmModal';
|
||||
|
||||
@@ -69,7 +70,9 @@ export const CommentModal: React.FC<CommentModalProps> = ({ isOpen, onClose, loc
|
||||
fetchComments();
|
||||
|
||||
// Lắng nghe bình luận mới qua Proxy (không cần hardcode URL)
|
||||
const socket = io();
|
||||
const socket = Capacitor.isNativePlatform()
|
||||
? io(import.meta.env.VITE_BACKEND_URL || 'https://yotrip.labz.io.vn')
|
||||
: io();
|
||||
socket.emit('joinTour', 'global'); // Hoặc logic join cụ thể
|
||||
|
||||
socket.on('commentAdded', (newCommentData: any) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { X, Mail, Lock, ArrowRight, Loader2, LogIn } from 'lucide-react';
|
||||
import { useNotification } from '@/hooks/useNotification';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth';
|
||||
|
||||
interface JoinTourLoginModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -119,6 +121,88 @@ export const JoinTourLoginModal: React.FC<JoinTourLoginModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleNativeGoogleLogin = async () => {
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const user = await GoogleAuth.signIn();
|
||||
console.log('[JoinTourLogin] Native Google user received:', user);
|
||||
|
||||
const idToken = user.authentication.idToken;
|
||||
if (!idToken) {
|
||||
throw new Error('Không nhận được token từ Google.');
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/v1/auth/google`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ credential: idToken }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Đăng nhập Google thất bại');
|
||||
}
|
||||
|
||||
localStorage.setItem('token', data.access_token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
|
||||
let currentToken = inviteTokenRef.current || localStorage.getItem('pendingInviteToken');
|
||||
if (!currentToken) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const urlToken = params.get('token');
|
||||
if (urlToken) {
|
||||
currentToken = urlToken;
|
||||
localStorage.setItem('pendingInviteToken', urlToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentToken) {
|
||||
throw new Error('Không có mã lời mời để tham gia tour');
|
||||
}
|
||||
|
||||
try {
|
||||
const joinRes = await fetch(`/api/v1/tours/join-by-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${data.access_token}`,
|
||||
},
|
||||
body: JSON.stringify({ token: currentToken }),
|
||||
});
|
||||
|
||||
const joinData = await joinRes.json().catch(() => ({}));
|
||||
|
||||
if (joinRes.ok) {
|
||||
notify({
|
||||
title: 'Thành công',
|
||||
message: joinData.message || 'Bạn đã gia nhập tour!',
|
||||
type: 'success'
|
||||
});
|
||||
localStorage.removeItem('pendingInviteToken');
|
||||
if (onJoinSuccess) {
|
||||
onJoinSuccess(data.user, joinData);
|
||||
}
|
||||
onClose();
|
||||
} else {
|
||||
localStorage.removeItem('pendingInviteToken');
|
||||
const errorMessage = joinData.message || `Không thể gia nhập tour (HTTP ${joinRes.status})`;
|
||||
setError(`Lỗi gia nhập tour: ${errorMessage}`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(`Lỗi khi gia nhập: ${e.message}`);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[JoinTourLogin] Native Google login error:', err);
|
||||
if (err?.message !== 'Sign in window was closed' && err?.message !== '12501') {
|
||||
setError(err.message || 'Đăng nhập Google thất bại');
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailPasswordJoin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
@@ -182,7 +266,7 @@ export const JoinTourLoginModal: React.FC<JoinTourLoginModalProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
if (!isOpen || Capacitor.isNativePlatform()) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (typeof window !== 'undefined' && (window as any).google) {
|
||||
@@ -241,10 +325,27 @@ export const JoinTourLoginModal: React.FC<JoinTourLoginModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Google OAuth Button */}
|
||||
<div className="mb-4 sm:mb-6 flex justify-center">
|
||||
<div id="google-signin-btn-join-tour" className="w-full" />
|
||||
</div>
|
||||
{Capacitor.isNativePlatform() ? (
|
||||
<div className="mb-4 sm:mb-6 flex justify-center w-full">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNativeGoogleLogin}
|
||||
className="w-full flex items-center justify-center gap-3 bg-white hover:bg-slate-50 text-slate-800 font-bold py-3.5 px-4 rounded-2xl border border-slate-200 transition-all active:scale-[0.98] cursor-pointer text-sm shadow-sm"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M12.24 10.285V14.4h6.887c-.648 2.41-2.519 4.114-5.136 4.114-3.51 0-6.357-2.89-6.357-6.457 0-3.568 2.848-6.458 6.357-6.458 1.614 0 3.08.618 4.19 1.625l3.03-3.03C19.045 2.11 15.895 1 12.24 1 6.033 1 1 6.033 1 12.24s5.033 11.24 11.24 11.24c5.84 0 10.74-4.14 11.24-9.84v-3.355H12.24z"
|
||||
/>
|
||||
</svg>
|
||||
Đăng nhập bằng Google
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-4 sm:mb-6 flex justify-center">
|
||||
<div id="google-signin-btn-join-tour" className="w-full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative mb-4 sm:mb-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Mail, Lock, ArrowRight, Loader2 } from 'lucide-react';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth';
|
||||
|
||||
interface LoginModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -101,8 +103,70 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose, onSwitc
|
||||
}
|
||||
};
|
||||
|
||||
const handleNativeGoogleLogin = async () => {
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const user = await GoogleAuth.signIn();
|
||||
console.log('[Native OAuth] Google user received:', user);
|
||||
|
||||
const idToken = user.authentication.idToken;
|
||||
if (!idToken) {
|
||||
throw new Error('Không nhận được token từ Google.');
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/v1/auth/google`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ credential: idToken }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Đăng nhập Google thất bại');
|
||||
}
|
||||
|
||||
localStorage.setItem('token', data.access_token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
localStorage.removeItem('guest_token');
|
||||
localStorage.removeItem('guest_user');
|
||||
console.log('[Native OAuth] Google login successful');
|
||||
|
||||
let pendingInviteToken = localStorage.getItem('pendingInviteToken');
|
||||
if (pendingInviteToken) {
|
||||
try {
|
||||
const joinRes = await fetch(`/api/v1/tours/join-by-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${data.access_token}`,
|
||||
},
|
||||
body: JSON.stringify({ token: pendingInviteToken }),
|
||||
});
|
||||
if (joinRes.ok) {
|
||||
localStorage.removeItem('pendingInviteToken');
|
||||
}
|
||||
} catch (joinErr) {
|
||||
console.error('[Native OAuth] Join tour after login failed:', joinErr);
|
||||
}
|
||||
}
|
||||
|
||||
if (onLoginSuccess) {
|
||||
onLoginSuccess(data.user);
|
||||
}
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
console.error('[Native OAuth] Error:', err);
|
||||
if (err?.message !== 'Sign in window was closed' && err?.message !== '12501') {
|
||||
setError(err.message || 'Đăng nhập Google thất bại');
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
if (!isOpen || Capacitor.isNativePlatform()) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (typeof window !== 'undefined' && (window as any).google) {
|
||||
@@ -244,7 +308,23 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose, onSwitc
|
||||
<span className="relative px-3 bg-[var(--surface)] text-xs font-bold text-[var(--text-muted)] uppercase">Hoặc</span>
|
||||
</div>
|
||||
|
||||
<div id="google-signin-btn-login" className="w-full flex justify-center"></div>
|
||||
{Capacitor.isNativePlatform() ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNativeGoogleLogin}
|
||||
className="w-full flex items-center justify-center gap-3 bg-white hover:bg-slate-50 text-slate-800 font-bold py-3.5 px-4 rounded-2xl border border-slate-200 transition-all active:scale-[0.98] cursor-pointer text-sm shadow-sm"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M12.24 10.285V14.4h6.887c-.648 2.41-2.519 4.114-5.136 4.114-3.51 0-6.357-2.89-6.357-6.457 0-3.568 2.848-6.458 6.357-6.458 1.614 0 3.08.618 4.19 1.625l3.03-3.03C19.045 2.11 15.895 1 12.24 1 6.033 1 1 6.033 1 12.24s5.033 11.24 11.24 11.24c5.84 0 10.74-4.14 11.24-9.84v-3.355H12.24z"
|
||||
/>
|
||||
</svg>
|
||||
Đăng nhập bằng Google
|
||||
</button>
|
||||
) : (
|
||||
<div id="google-signin-btn-login" className="w-full flex justify-center"></div>
|
||||
)}
|
||||
|
||||
<div className="mt-10 pt-8 border-t border-[var(--border)] text-center">
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Camera as CameraIcon, X } from 'lucide-react';
|
||||
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
|
||||
import { useNotification } from '@/hooks/useNotification';
|
||||
|
||||
interface PhotoTakerProps {
|
||||
onPhotoTaken?: (webPath: string) => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const PhotoTaker: React.FC<PhotoTakerProps> = ({ onPhotoTaken, onClose }) => {
|
||||
const [photoUri, setPhotoUri] = useState<string | undefined>();
|
||||
const notify = useNotification();
|
||||
|
||||
const takeAndSavePhoto = async () => {
|
||||
try {
|
||||
const image = await Camera.getPhoto({
|
||||
quality: 90,
|
||||
allowEditing: false, // Giữ nguyên ảnh gốc, không qua chỉnh sửa
|
||||
resultType: CameraResultType.Uri,
|
||||
source: CameraSource.Camera, // Mở camera trực tiếp
|
||||
saveToGallery: true, // Tự động lưu ảnh gốc vào thư viện điện thoại
|
||||
});
|
||||
|
||||
setPhotoUri(image.webPath);
|
||||
notify({
|
||||
title: 'Thành công',
|
||||
message: 'Ảnh đã được chụp và tự động lưu vào thư viện điện thoại.',
|
||||
type: 'success'
|
||||
});
|
||||
if (onPhotoTaken && image.webPath) {
|
||||
onPhotoTaken(image.webPath);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Lỗi khi chụp hoặc lưu ảnh:', error);
|
||||
if (error?.message !== 'User cancelled photos app') {
|
||||
notify({
|
||||
title: 'Lỗi chụp ảnh',
|
||||
message: 'Không thể truy cập camera hoặc lưu ảnh.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-6 bg-slate-900/60 border border-slate-800/80 rounded-2xl w-full max-w-md mx-auto backdrop-blur-md">
|
||||
<div className="flex justify-between items-center w-full mb-4">
|
||||
<h3 className="text-sm font-black uppercase text-indigo-400 tracking-wider">Chụp ảnh hành trình</h3>
|
||||
{onClose && (
|
||||
<button onClick={onClose} className="p-1 hover:bg-slate-800 rounded-full transition-colors text-slate-400 hover:text-white">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={takeAndSavePhoto}
|
||||
className="w-full flex items-center justify-center gap-2 py-3 px-4 bg-indigo-650 hover:bg-indigo-700 active:scale-[0.98] text-white rounded-xl font-bold transition-all shadow-md cursor-pointer text-xs uppercase tracking-wider"
|
||||
>
|
||||
<CameraIcon className="w-4 h-4" />
|
||||
Chụp và Lưu Ảnh Gốc
|
||||
</button>
|
||||
|
||||
{photoUri && (
|
||||
<div className="mt-6 w-full text-center border border-slate-800/85 bg-slate-950/40 p-4 rounded-xl">
|
||||
<p className="text-[11px] text-slate-400 mb-2 font-semibold">Xem trước ảnh vừa chụp:</p>
|
||||
<img src={photoUri} alt="Xem trước ảnh chụp" className="max-w-full h-auto rounded-lg mx-auto border border-slate-800" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { X, Send, MessageSquare, User, Loader2, Calendar, MapPin, Download, Edit, Heart, Trash2 } from 'lucide-react';
|
||||
import { io } from 'socket.io-client';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { CoordinateSelectModal } from './CoordinateSelectModal';
|
||||
import { useTranslation } from '../hooks/useTranslation';
|
||||
import { useConfirm } from '../hooks/useConfirm';
|
||||
@@ -246,7 +247,9 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
|
||||
fetchComments();
|
||||
|
||||
const socket = io();
|
||||
const socket = Capacitor.isNativePlatform()
|
||||
? io(import.meta.env.VITE_BACKEND_URL || 'https://yotrip.labz.io.vn')
|
||||
: io();
|
||||
socket.emit('joinPhoto', photo.id);
|
||||
|
||||
socket.on('photoCommentAdded', (newCommentData: any) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { Send, MessageSquare, Loader2, Image as ImageIcon, MapPin, Download, X } from 'lucide-react';
|
||||
import { useNotification } from '@/hooks/useNotification';
|
||||
|
||||
@@ -32,13 +33,13 @@ export const TourChat: React.FC<TourChatProps> = ({ tourId, embedded = false })
|
||||
const mentionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const getHeaders = () => ({
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
'Authorization': `Bearer ${localStorage.getItem('token') || localStorage.getItem('guest_token')}`,
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
|
||||
const currentUserId = (() => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem('token') || localStorage.getItem('guest_token');
|
||||
if (!token) return null;
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
@@ -87,7 +88,7 @@ export const TourChat: React.FC<TourChatProps> = ({ tourId, embedded = false })
|
||||
}, []);
|
||||
|
||||
const filteredParticipants = participants.filter(p =>
|
||||
p.name.toLowerCase().includes(mentionSearch.toLowerCase())
|
||||
p && p.name && p.name.toLowerCase().includes(mentionSearch.toLowerCase())
|
||||
);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -179,7 +180,9 @@ export const TourChat: React.FC<TourChatProps> = ({ tourId, embedded = false })
|
||||
|
||||
// Connect to socket and listen for tour messages
|
||||
useEffect(() => {
|
||||
const socket = io();
|
||||
const socket = Capacitor.isNativePlatform()
|
||||
? io(import.meta.env.VITE_BACKEND_URL || 'https://yotrip.labz.io.vn')
|
||||
: io();
|
||||
socketRef.current = socket;
|
||||
|
||||
socket.on('connect', () => {
|
||||
@@ -311,7 +314,7 @@ export const TourChat: React.FC<TourChatProps> = ({ tourId, embedded = false })
|
||||
const res = await fetch('/api/v1/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
'Authorization': `Bearer ${localStorage.getItem('token') || localStorage.getItem('guest_token')}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import './utils/nativePatch';
|
||||
import App from './App.js';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import {
|
||||
Compass,
|
||||
Users,
|
||||
@@ -540,7 +541,9 @@ export const MemberDashboard: React.FC<MemberDashboardProps> = ({
|
||||
if (!user?.id) return;
|
||||
|
||||
// Connect to WebSocket using same origin/proxy
|
||||
const socket = io();
|
||||
const socket = Capacitor.isNativePlatform()
|
||||
? io(import.meta.env.VITE_BACKEND_URL || 'https://yotrip.labz.io.vn')
|
||||
: io();
|
||||
socketRef.current = socket;
|
||||
|
||||
socket.on('connect', () => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { User, Mail, Lock, ArrowRight, ChevronLeft, Phone, MapPin, ShieldCheck } from 'lucide-react';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth';
|
||||
|
||||
interface SignupPageProps {
|
||||
onBack: () => void;
|
||||
@@ -69,8 +71,67 @@ const SignupPage: React.FC<SignupPageProps> = ({ onBack, onSuccess }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleNativeGoogleLogin = async () => {
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const user = await GoogleAuth.signIn();
|
||||
console.log('[SignupPage] Native Google user received:', user);
|
||||
|
||||
const idToken = user.authentication.idToken;
|
||||
if (!idToken) {
|
||||
throw new Error('Không nhận được token từ Google.');
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/v1/auth/google`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ credential: idToken }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Đăng ký bằng Google thất bại');
|
||||
}
|
||||
|
||||
localStorage.removeItem('guest_token');
|
||||
localStorage.removeItem('guest_user');
|
||||
localStorage.setItem('token', data.access_token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
|
||||
const pendingInviteToken = localStorage.getItem('pendingInviteToken');
|
||||
if (pendingInviteToken) {
|
||||
console.log('[SignupPage] Auto-joining tour with pending token after Google signup');
|
||||
await fetch(`/api/v1/tours/join-by-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${data.access_token}`,
|
||||
},
|
||||
body: JSON.stringify({ token: pendingInviteToken }),
|
||||
}).then(res => {
|
||||
if (res.ok) {
|
||||
localStorage.removeItem('pendingInviteToken');
|
||||
console.log('[SignupPage] Successfully joined tour after Google signup');
|
||||
} else {
|
||||
console.warn('[SignupPage] Failed to join tour after Google signup:', res.status);
|
||||
}
|
||||
}).catch((e) => console.error('Lỗi tự động gia nhập:', e));
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
} catch (err: any) {
|
||||
console.error('[SignupPage] Native Google signup error:', err);
|
||||
if (err?.message !== 'Sign in window was closed' && err?.message !== '12501') {
|
||||
setError(err.message || 'Đăng ký bằng Google thất bại');
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== 'form') return;
|
||||
if (step !== 'form' || Capacitor.isNativePlatform()) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (typeof window !== 'undefined' && (window as any).google) {
|
||||
@@ -313,7 +374,23 @@ const SignupPage: React.FC<SignupPageProps> = ({ onBack, onSuccess }) => {
|
||||
<span className="relative px-3 bg-[var(--surface)] text-xs font-bold text-[var(--text-muted)] uppercase">Hoặc</span>
|
||||
</div>
|
||||
|
||||
<div id="google-signin-btn-signup" className="w-full flex justify-center"></div>
|
||||
{Capacitor.isNativePlatform() ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNativeGoogleLogin}
|
||||
className="w-full flex items-center justify-center gap-3 bg-white hover:bg-slate-50 text-slate-800 font-bold py-3.5 px-4 rounded-2xl border border-slate-200 transition-all active:scale-[0.98] cursor-pointer text-sm shadow-sm"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M12.24 10.285V14.4h6.887c-.648 2.41-2.519 4.114-5.136 4.114-3.51 0-6.357-2.89-6.357-6.457 0-3.568 2.848-6.458 6.357-6.458 1.614 0 3.08.618 4.19 1.625l3.03-3.03C19.045 2.11 15.895 1 12.24 1 6.033 1 1 6.033 1 12.24s5.033 11.24 11.24 11.24c5.84 0 10.74-4.14 11.24-9.84v-3.355H12.24z"
|
||||
/>
|
||||
</svg>
|
||||
Đăng ký bằng Google
|
||||
</button>
|
||||
) : (
|
||||
<div id="google-signin-btn-signup" className="w-full flex justify-center"></div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { io } from 'socket.io-client';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { jsPDF } from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import { robotoBase64, robotoBoldBase64 } from '../utils/pdfFont';
|
||||
@@ -1564,7 +1565,9 @@ export const TourDetailPage = ({
|
||||
useEffect(() => {
|
||||
if (!currentTour) return;
|
||||
|
||||
const socket = io(); // Kết nối qua Proxy của Vite (cùng origin)
|
||||
const socket = Capacitor.isNativePlatform()
|
||||
? io(import.meta.env.VITE_BACKEND_URL || 'https://yotrip.labz.io.vn')
|
||||
: io(); // Kết nối qua Proxy của Vite (cùng origin)
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.emit('joinTour', currentTour.id);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth';
|
||||
|
||||
function rewriteUrls(obj: any, backendUrl: string): any {
|
||||
if (obj === null || obj === undefined) return obj;
|
||||
if (typeof obj === 'string') {
|
||||
if (obj.startsWith('/uploads/')) {
|
||||
return `${backendUrl}${obj}`;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => rewriteUrls(item, backendUrl));
|
||||
}
|
||||
if (typeof obj === 'object') {
|
||||
const newObj: any = {};
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
newObj[key] = rewriteUrls(obj[key], backendUrl);
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
// Initialize native Google Login client to prevent NullPointerException crashes
|
||||
try {
|
||||
GoogleAuth.initialize({
|
||||
clientId: import.meta.env.VITE_GOOGLE_CLIENT_ID || '639044397050-7hair384u6lle941h033trut1s3q47l4.apps.googleusercontent.com',
|
||||
scopes: ['profile', 'email'],
|
||||
grantOfflineAccess: true,
|
||||
});
|
||||
console.log('[Native OAuth] GoogleAuth initialized successfully.');
|
||||
} catch (e) {
|
||||
console.error('[Native OAuth] Failed to initialize GoogleAuth client:', e);
|
||||
}
|
||||
|
||||
const backendUrl = import.meta.env.VITE_BACKEND_URL || 'https://yotrip.labz.io.vn';
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
let url = input;
|
||||
if (typeof url === 'string') {
|
||||
if (url.startsWith('/api/') || url.startsWith('/uploads/')) {
|
||||
url = `${backendUrl}${url}`;
|
||||
}
|
||||
} else if (url instanceof URL) {
|
||||
if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/uploads/')) {
|
||||
url = new URL(`${backendUrl}${url.pathname}${url.search}`);
|
||||
}
|
||||
} else if (url && typeof url === 'object' && 'url' in url) {
|
||||
// If it is a Request object
|
||||
const req = url as Request;
|
||||
const reqUrl = req.url;
|
||||
if (reqUrl.startsWith('/') || new URL(reqUrl).pathname.startsWith('/api/') || new URL(reqUrl).pathname.startsWith('/uploads/')) {
|
||||
const targetUrl = reqUrl.startsWith('/') ? `${backendUrl}${reqUrl}` : reqUrl.replace(new URL(reqUrl).origin, backendUrl);
|
||||
url = new Request(targetUrl, req);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await originalFetch(url, init);
|
||||
|
||||
// Override the json method of this specific response instance
|
||||
const originalJson = response.json;
|
||||
response.json = async () => {
|
||||
const data = await originalJson.call(response);
|
||||
return rewriteUrls(data, backendUrl);
|
||||
};
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user