@astralweb/nova-social-login
v2.0.3
Published
Social Login module for Nova e-commerce platform (Internal Use Only)
Readme
Social Login 模組使用指南
⚠️ 內部使用套件 - 此套件僅供 Astral Web 內部使用,未經授權不得用於其他用途。
概覽
Social Login 是一個 Nuxt 3 模組,提供完整的第三方社群登入功能,支援 Google、Facebook、LINE 等平台的 OAuth 認證。模組採用 Nuxt Module 架構設計,支援自動註冊組件、composables,並提供靈活的樣式配置、服務端回調處理,以及社群帳號綁定/解綁功能。
安裝與設定
1. 安裝套件
在 apps/web 和 apps/server 安裝
yarn add @astralweb/nova-social-login2. Middleware 擴充
// apps/server/extendApiMethods/index.ts
// ... 略
export {
socialLoginV2,
socialLoginV2Binding,
socialLoginV2Unbinding,
customerSocialAccounts,
} from '@astralweb/nova-social-login/api';3. GraphQL 擴充
// apps/server/extendApiMethods/customer.ts
export const generateCustomerToken = async (
context: Context,
variables: TObject,
): Promise<FetchResult<GenerateCustomerTokenMutation>> => {
const { res, config, client } = context;
try {
const result = await client.mutate({
mutation: gql`
mutation generateCustomerToken($type: Int!, $email: String, $password: String, $uid: String) {
generateCustomerToken(type: $type, email: $email, password: $password, uid: $uid) {
token
}
}
`,
variables,
context: {
headers: getHeaders(context),
},
});
// ... 略
return result;
} catch (error) {
// ... 略
}
};4. 註冊模組
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@astralweb/nova-social-login',
],
})5. storeConfig customQuery 新增欄位
// apps/web/api/magento/storeConfig/customQuery.ts
import { socialLoginFields } from '@astralweb/nova-social-login/api';
export const storeConfigCustomQuery: Record<string, any> = {
storeConfig: 'store-config-custom-query',
metadata: {
fields: `
product_reviews_enabled
allow_guests_to_write_product_reviews
is_guest_checkout_enabled
cart_expires_in_days
default_description
default_title
default_country
${socialLoginFields}
`,
},
};6. Pinia storeConfig 設定
// apps/web/stores/storeConfig.ts
export const useStoreConfigStore = defineStore('storeConfig', {
getters: {
// ... 略
socialLoginEnabled(state) {
const socialLoginPlatforms = Object.keys(state.storeConfig?.social_login_v2 ?? {});
return socialLoginPlatforms.length > 0;
},
socialLoginConfig(state) {
return state.storeConfig?.social_login_v2 ?? null;
},
},
});7. Account Modal 新增功能
// apps/web/plugins/account-modal.ts
export default defineNuxtPlugin(() => {
// ... 略
const closeAccountModal = () => {
close();
activeTab.value = 'login';
if (isSocialLoginRegister.value) {
initialRegisterValues.value = {};
}
};
const initialRegisterValues = ref<Partial<ExtendCustomerCreateInput>>({});
const openSocialLoginRegisterModal = (initialValues: Partial<ExtendCustomerCreateInput>) => {
activeTab.value = 'socialLoginRegister';
initialRegisterValues.value = initialValues;
nextTick(() => {
openAccountModal();
});
};
const isSocialLoginRegister = computed(() => activeTab.value === 'socialLoginRegister' && initialRegisterValues.value?.type);
return {
provide: {
accountModal: {
// ... 略
openSocialLoginRegisterModal,
initialRegisterValues,
isSocialLoginRegister,
},
},
};
});// apps/web/index.d.ts
import type { Ref } from 'vue';
import type { ExtendCustomerCreateInput } from '@astralweb/nova-magento-types';
declare module '#app' {
interface NuxtApp {
$accountModal: {
// ... 略
openSocialLoginRegisterModal: (initialValues: Partial<ExtendCustomerCreateInput>) => void;
initialRegisterValues: Ref<Partial<ExtendCustomerCreateInput>>;
isSocialLoginRegister: Ref<boolean>;
};
}
}8. Composable 新增 useIntegrationSocialLogin
// apps/web/composables/useIntegrationSocialLogin/useIntegrationSocialLogin.ts
import { useSocialLoginVerification } from '@astralweb/nova-social-login';
export const useIntegrationSocialLogin = () => {
const { $accountModal } = useNuxtApp();
const { openAccountModal, openSocialLoginRegisterModal } = $accountModal;
const { customerLoginMutation } = useCustomer();
const toast = useToast();
const { t } = useI18n();
const { isLoggedIn } = useCustomerStore();
const { processLoginResult } = useSocialLoginVerification();
const oauthProvider = useCookie('oauth-provider');
const oauthCode = useCookie('oauth-code');
onMounted(async () => {
// 如果已登入,則改走 social login binding
if (isLoggedIn) return;
if (!oauthProvider.value || !oauthCode.value) {
return;
}
await processLoginResult({
handleExistCustomer: customerLoginMutation.mutate,
openLoginModal: () => {
openAccountModal();
// 避免被 modal 遮住
nextTick(() => {
toast.add({
severity: 'info',
summary: t('socialLogin.alreadyHaveAccount'),
detail: t('socialLogin.useEmailLogin'),
life: 5000,
});
});
},
handleNewCustomer: openSocialLoginRegisterModal,
handleError: (error: string) => {
openAccountModal();
nextTick(() => {
toast.add({
severity: 'error',
summary: t('socialLogin.failed'),
detail: error,
life: 5000,
});
});
},
});
});
};// apps/web/composables/useIntegrationSocialLogin/index.ts
export * from './useIntegrationSocialLogin';9. 使用 useIntegrationSocialLogin
<!-- apps/web/app.vue -->
<script setup lang="ts">
// ... 略
onMounted(() => {
// Need this class for cypress testing
bodyClass.value = 'hydrated';
useIntegrationSocialLogin();
});
</script><!-- apps/web/error.vue -->
<script setup lang="ts">
// ... 略
onMounted(() => {
useIntegrationSocialLogin();
});
</script>組件基本使用
社群登入
在登入頁面引入社群登入按鈕
<!-- AccountFormsLogin -->
<template>
<div class="inline-flex w-fit flex-col gap-y-3 xl:mt-4 xl:gap-y-5">
<div class="flex items-center justify-center">
<div class="grow border-t border-primary-200"></div>
<span class="typography-text-base mx-4 text-primary-700">
{{ t('account.socialLogin') }}
</span>
<div class="grow border-t border-primary-200"></div>
</div>
<NovaSocialLoginButtonGroup
v-if="storeConfigStore.socialLoginConfig"
:social-login-config="storeConfigStore.socialLoginConfig"
/>
</div>
</template>
<script setup lang="ts">
const storeConfigStore = useStoreConfigStore();
</script>社群註冊
使用原本的註冊表單,並且新增標題
<!-- components/Account/Modal/AccountModal.vue -->
<template>
<!-- ... 略 -->
<template #header>
<div v-if="isSocialLoginRegister" class="w-full text-center text-secondary-700">
{{ t('socialLogin.register.heading') }}
</div>
<PrimeTabs
v-else-if="showTabs"
:value="activeTab"
class="w-full"
scrollable
>
<PrimeTabList
class="mt-4"
:pt="{
root: ({ instance }: any) => {
setReferenceTabListContent(instance.$refs.content);
},
}"
>
<PrimeTab
v-for="(tab, index) in tabs"
:key="tab.label"
:ref="(el: any) => setReferenceTab(el, index)"
:value="tab.value"
class="typography-text-sm w-[142px] xl:w-[120px]"
@click="updateActiveTab(tab.value, index)"
>
{{ tab.label }}
</PrimeTab>
</PrimeTabList>
</PrimeTabs>
<div v-else class="w-full text-center text-secondary-700">
{{ t('auth.forgotPassword.heading') }}
</div>
</template>
<!-- ... 略 -->
</template>
<script setup lang="ts">
// ... 略
const formComponent = computed(() => {
const forms: Record<FormComponentName, Component> = {
login: AccountFormsLogin,
register: AccountFormsRegister,
forgetPassword: AccountFormsForgetPassword,
socialLoginRegister: AccountFormsRegister,
};
return forms[activeTab.value as FormComponentName];
});
</script>修改 createCustomer input 的格式
<!-- components/Account/Forms/AccountFormsRegister.vue -->
<script setup lang="ts">
// ... 略
const { $accountModal } = useNuxtApp();
const { initialRegisterValues, isSocialLoginRegister } = $accountModal;
const onSubmit = handleSubmit((values) => {
const {
lastname, email, password, gender, date_of_birth, phone,
} = values;
const baseInput = {
lastname,
email,
gender,
dob: formatDate(date_of_birth),
is_subscribed: isSubscribed.value,
phone,
};
const input: ExtendCustomerCreateInput = isSocialLoginRegister.value
? {
...baseInput,
type: initialRegisterValues.value.type!,
uid: initialRegisterValues.value.uid,
}
: {
...baseInput,
type: 0,
password,
};
createCustomerMutate(input);
});
</script>社群帳號綁定組件
<!-- social binding page -->
<template>
<NuxtLayout name="account">
<p class="typography-text-sm mb-4 text-primary-800 xl:mb-5">
{{ t('socialLogin.binding.description') }}
</p>
<NovaSocialBindingButtonGroup
v-if="storeConfigStore.socialLoginConfig"
:social-login-config="storeConfigStore.socialLoginConfig"
:t="t"
:button-size="isDesktop ? '' : 'small'"
@on-binding-success="handleSocialBindingSuccess"
@on-binding-error="handleSocialBindingError"
@on-unbinding-success="handleSocialUnbindingSuccess"
@on-unbinding-error="handleSocialUnbindingError"
/>
</NuxtLayout>
</template>
<script setup lang="ts">
definePageMeta({
name: 'customerSocialBinding',
layout: false,
});
const { t } = useI18n();
const toast = useToast();
const { isDesktop } = useBreakpoints();
const storeConfigStore = useStoreConfigStore();
const { socialLoginEnabled } = useStoreConfigStore();
if (!socialLoginEnabled) {
throw createError({
statusCode: 404,
statusMessage: 'Page Not Found',
});
}
const handleSocialBindingSuccess = () => {
toast.add({
severity: 'success',
summary: t('socialLogin.binding.success'),
life: 5000,
});
};
const handleSocialBindingError = (error: Error) => {
toast.add({
severity: 'error',
summary: t('socialLogin.binding.error'),
detail: error.message,
life: 5000,
});
};
const handleSocialUnbindingSuccess = () => {
toast.add({
severity: 'success',
summary: t('socialLogin.unbinding.success'),
life: 5000,
});
};
const handleSocialUnbindingError = (error: Error) => {
toast.add({
severity: 'error',
summary: t('socialLogin.unbinding.error'),
detail: error.message,
life: 5000,
});
};
</script>自動註冊的組件
模組會自動註冊以下組件,可直接在模板中使用:
<NovaSocialLoginButtonGroup />- 社群登入按鈕群組<NovaSocialBindingButtonGroup />- 社群帳號綁定介面<IconFacebook />- Facebook 圖示<IconFacebookFilled />- Facebook 填充圖示<IconGoogle />- Google 圖示<IconLine />- LINE 圖示
Composables API
useSocialLogin
核心 API composable,提供所有社群登入相關的 API 方法。
const {
fetchSocialLoginV2,
fetchCustomerSocialAccount,
socialBinding,
socialUnbinding
} = useSocialLogin();
// 驗證社群登入
const result = await fetchSocialLoginV2({
sns_type: 1, // 1: Google, 2: LINE, 3: Facebook
auth_code: 'xxx'
});
// 獲取用戶社群帳號列表
const accounts = await fetchCustomerSocialAccount();
// 綁定社群帳號
await socialBinding({ sns_type: 1, auth_code: 'xxx' });
// 解綁社群帳號
await socialUnbinding({ sns_type: 1 });useSocialLoginOAuth
處理 OAuth 流程
const {
handleOAuthConnect,
setOAuthRedirectUri,
generateState,
buildOAuthUrl
} = useSocialLoginOAuth(config);
// 設定登入成功後的重定向 URL
setOAuthRedirectUri();
// 發起 OAuth 連接
await handleOAuthConnect('google');useSocialBinding
社群帳號綁定管理
const {
fetchCustomerSocialAccountsQuery,
socialBindingMutation,
socialUnbindingMutation,
isOAuthCallback
} = useSocialBinding();
// 使用 Vue Query 獲取社群帳號
const { data: accounts } = fetchCustomerSocialAccountsQuery();
// 綁定 mutation
const { mutate: bind } = socialBindingMutation();
// 解綁 mutation
const { mutate: unbind } = socialUnbindingMutation();useSocialLoginVerification
處理登入驗證流程
const { processLoginResult } = useSocialLoginVerification();
// 處理 OAuth 回調結果
await processLoginResult({
handleExistCustomer: (data) => { /* 處理已存在用戶 */ },
openLoginModal: () => { /* 開啟登入彈窗 */ },
handleNewCustomer: (data) => { /* 處理新用戶註冊 */ },
handleError: (error) => { /* 錯誤處理 */ }
});useSocialLoginButtonGroupStyle
登入按鈕樣式配置
const {
containerClasses,
containerStyles,
buttonClasses,
buttonStyles,
iconClasses,
iconStyles,
textClasses,
textStyles,
} = useSocialLoginButtonGroupStyle(styleConfig);useSocialBindingButtonGroupStyle
綁定介面樣式配置
const {
containerClasses,
containerStyles,
cardClasses,
cardStyles,
labelClasses,
labelStyles,
buttonClasses,
buttonStyles,
bindingButtonClasses,
bindingButtonStyles,
iconClasses,
iconStyles,
textClasses,
textStyles,
} = useSocialBindingButtonGroupStyle(styleConfig);樣式配置
社群帳號登入樣式配置
組件支援通過 style-config prop 自訂樣式
<template>
<NovaSocialLoginButtonGroup
:social-login-config="storeConfigStore.socialLoginConfig"
:style-config="customStyleConfig"
:show-text="true"
/>
</template>
<script setup lang="ts">
const customStyleConfig = {
// 容器樣式
containerClass: 'grid grid-cols-1 md:grid-cols-3 gap-6',
containerStyle: {
padding: '1rem',
backgroundColor: '#f9fafb'
},
// 按鈕樣式
buttonClass: 'w-full py-3 px-6 rounded-xl font-semibold transition-all duration-200 hover:scale-105',
buttonStyle: {
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
border: 'none'
},
// 圖示樣式
iconClass: 'w-6 h-6',
iconStyle: {
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))'
},
// 文字樣式
textClass: 'text-lg font-medium',
textStyle: {
letterSpacing: '0.025em'
}
};
</script>社群帳號綁定樣式配置
<template>
<NovaSocialBindingButtonGroup
:social-login-config="socialLoginConfig"
:style-config="bindingStyleConfig"
/>
</template>
<script setup lang="ts">
const bindingStyleConfig = {
// 容器樣式
containerClass: 'max-w-2xl mx-auto space-y-6',
// 卡片樣式
cardClass: 'bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden',
cardStyle: {
transition: 'all 0.3s ease',
},
// 標籤區域樣式
labelClass: 'flex items-center gap-4 p-6',
// 綁定按鈕樣式
bindingButtonClass: 'px-6 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors',
// 解綁按鈕樣式
buttonClass: 'px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors',
// 圖示樣式
iconClass: 'w-8 h-8',
// 文字樣式
textClass: 'text-xl font-semibold text-gray-800'
};
</script>Server Routes
模組會自動註冊以下 server route:
- GET
/social-login/callback- OAuth 回調處理- 此路由處理 OAuth provider 的回調,並將認證結果存儲在 cookie 中。
組件 API 參考
NovaSocialLoginButtonGroup
Props
| 屬性 | 類型 | 預設值 | 說明 |
|------|------|--------|------|
| socialLoginConfig | SocialLoginConfig | - | 社群登入配置(必填) |
| styleConfig | SocialLoginButtonGroupStyleConfig | {} | 樣式配置物件 |
| showText | boolean | false | 是否顯示按鈕文字 |
| t | (key: string) => string | (key) => key | 國際化函數 |
Slots
每個社群都有獨立的插槽,可以完全替換該社群的按鈕
| 插槽名稱 | 作用域參數 | 說明 |
|---------|------------|------|
| google | { provider, handleOAuthLogin, clientId } | 自定義 Google 登入按鈕 |
| facebook | { provider, handleOAuthLogin, clientId } | 自定義 Facebook 登入按鈕 |
| line | { provider, handleOAuthLogin, clientId } | 自定義 LINE 登入按鈕 |
在預設按鈕內部可以自定義特定元素
| 插槽名稱 | 說明 |
|---------|------|
| icon | 自定義按鈕圖示 |
| text | 自定義按鈕文字 |
NovaSocialBindingButtonGroup
Props
| 屬性 | 類型 | 預設值 | 說明 |
|------|------|--------|------|
| socialLoginConfig | SocialBindingConfig | - | 社群登入配置(必填) |
| styleConfig | SocialBindingButtonGroupStyleConfig | {} | 樣式配置物件 |
| t | (key: string) => string | (key) => key | 國際化函數 |
| buttonSize | string | '' | 按鈕尺寸(支援 PrimeVue Button 的 size 屬性) |
Slots
每個社群都有獨立的插槽,可以完全替換該社群的綁定卡片
| 插槽名稱 | 作用域參數 | 說明 |
|---------|------------|------|
| google | { provider, clientId } | 自定義 Google 綁定卡片 |
| facebook | { provider, clientId } | 自定義 Facebook 綁定卡片 |
| line | { provider, clientId } | 自定義 LINE 綁定卡片 |
在預設卡片內部可以自定義特定元素
| 插槽名稱 | 說明 |
|---------|------|
| label | 自定義標籤區域(包含圖示和名稱) |
| icon | 自定義社群圖示 |
| text | 自定義社群名稱 |
| button | 自定義操作按鈕 |
Events
| 事件名稱 | 參數 | 說明 |
|---------|------|------|
| onBindingSuccess | - | 帳號綁定成功 |
| onBindingError | error: Error | 帳號綁定失敗 |
| onUnbindingSuccess | - | 帳號解綁成功 |
| onUnbindingError | error: Error | 帳號解綁失敗 |
類型定義
SocialLoginConfig
interface SocialLoginConfig {
google?: {
client_id: string;
sort_order: number;
uid?: string; // 已綁定時才有
};
facebook?: {
client_id: string;
sort_order: number;
uid?: string;
};
line?: {
client_id: string;
sort_order: number;
uid?: string;
promote_line_oa?: boolean; // LINE 官方帳號推廣
};
}SocialLoginType
enum SocialLoginType {
REGULAR = 0,
GOOGLE = 1,
LINE = 2,
FACEBOOK = 3,
}授權聲明
此套件僅供 Astral Web 內部使用,未經授權不得用於其他用途。
