feat: 애플 로그인 네이티브 연동 (Sign in with Apple)
@capacitor-community/apple-sign-in 으로 identityToken 획득 → /api/auth/apple. 애플 버튼은 네이티브 iOS 에서만 노출, 시트 취소는 무시. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
// Sign in with Apple — 네이티브(iOS) 전용 래퍼.
|
||||
// @capacitor-community/apple-sign-in 플러그인으로 identityToken(JWT)을 받아온다.
|
||||
// 이 토큰의 aud 는 앱 번들 ID(=capacitor appId)이며, 백엔드가 APPLE_CLIENT_ID 로 검증한다.
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { SignInWithApple } from '@capacitor-community/apple-sign-in'
|
||||
|
||||
// capacitor.config 의 appId 와 일치. iOS 네이티브 플로우의 aud 가 된다.
|
||||
const APPLE_CLIENT_ID = 'kr.sblog.dognation'
|
||||
|
||||
export interface AppleCredential {
|
||||
identityToken: string
|
||||
/** 애플은 최초 인증 시에만 이름을 준다. 이후에는 null. */
|
||||
name: string | null
|
||||
}
|
||||
|
||||
/** 현재 실행 환경이 네이티브(iOS)인지 — 애플 로그인 지원 여부. */
|
||||
export function isAppleSignInAvailable(): boolean {
|
||||
return Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'ios'
|
||||
}
|
||||
|
||||
/**
|
||||
* 네이티브 Sign in with Apple 을 실행하고 identityToken 을 반환한다.
|
||||
* 사용자가 취소하면 플러그인이 reject 하므로 호출부에서 구분 처리한다.
|
||||
*/
|
||||
export async function signInWithApple(): Promise<AppleCredential> {
|
||||
const { response } = await SignInWithApple.authorize({
|
||||
clientId: APPLE_CLIENT_ID,
|
||||
// iOS 네이티브 플로우에서는 사용되지 않지만 타입상 필요.
|
||||
redirectURI: `https://${APPLE_CLIENT_ID}/apple/callback`,
|
||||
scopes: 'name email',
|
||||
})
|
||||
// familyName + givenName (한국식). 최초 인증에만 제공되며, 없으면 null.
|
||||
const name =
|
||||
[response.familyName, response.givenName].filter(Boolean).join(' ').trim() || null
|
||||
return { identityToken: response.identityToken, name }
|
||||
}
|
||||
+21
-10
@@ -15,8 +15,9 @@
|
||||
{{ googleError }}
|
||||
</div>
|
||||
|
||||
<!-- 애플 로그인 -->
|
||||
<!-- 애플 로그인 (네이티브 iOS 전용) -->
|
||||
<q-btn
|
||||
v-if="appleAvailable"
|
||||
class="full-width q-py-sm"
|
||||
color="black"
|
||||
text-color="white"
|
||||
@@ -56,6 +57,7 @@ import { useQuasar } from 'quasar'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { authApi } from '@/api/auth'
|
||||
import { ApiError } from '@/api/http'
|
||||
import { isAppleSignInAvailable, signInWithApple } from '@/lib/appleAuth'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -65,6 +67,7 @@ const auth = useAuthStore()
|
||||
const googleBtn = ref<HTMLElement | null>(null)
|
||||
const googleError = ref('')
|
||||
const appleLoading = ref(false)
|
||||
const appleAvailable = isAppleSignInAvailable()
|
||||
const devLoading = ref(false)
|
||||
const isDev = import.meta.env.DEV
|
||||
|
||||
@@ -117,16 +120,24 @@ function loadGsi(): Promise<void> {
|
||||
}
|
||||
|
||||
async function onApple() {
|
||||
// 네이티브(iOS)에서는 Capacitor Sign in with Apple 플러그인으로 identityToken 을 받아
|
||||
// auth.loginApple(identityToken, name) 을 호출하도록 연동 예정.
|
||||
appleLoading.value = true
|
||||
$q.notify({
|
||||
color: 'grey-8',
|
||||
message: '애플 로그인은 네이티브(iOS) 플러그인 연동이 필요합니다.',
|
||||
icon: 'mdi-apple',
|
||||
position: 'top',
|
||||
})
|
||||
appleLoading.value = false
|
||||
try {
|
||||
const { identityToken, name } = await signInWithApple()
|
||||
await auth.loginApple(identityToken, name)
|
||||
redirectAfterLogin()
|
||||
} catch (e) {
|
||||
// 사용자가 시트를 취소한 경우는 조용히 무시.
|
||||
if (isAppleCancel(e)) return
|
||||
notifyError(e)
|
||||
} finally {
|
||||
appleLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 애플 로그인 시트 취소 판별 — 플러그인이 취소를 에러로 reject 한다. */
|
||||
function isAppleCancel(e: unknown): boolean {
|
||||
const msg = (e instanceof Error ? e.message : String(e)).toLowerCase()
|
||||
return msg.includes('cancel') || msg.includes('1001') || msg.includes('popup_closed')
|
||||
}
|
||||
|
||||
async function onDevLogin() {
|
||||
|
||||
Reference in New Issue
Block a user