@anker-in/campaign-ui 0.5.0-beta.4 → 0.5.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/credits/context/hooks/useCountries.js +1 -1
- package/dist/cjs/components/credits/context/hooks/useCountries.js.map +3 -3
- package/dist/cjs/components/memberPopup/MemberPopup.js +1 -1
- package/dist/cjs/components/memberPopup/MemberPopup.js.map +3 -3
- package/dist/cjs/components/memberPopup/panels/ActivitiesSubscribePanel.d.ts +10 -0
- package/dist/cjs/components/memberPopup/panels/ActivitiesSubscribePanel.js +2 -0
- package/dist/cjs/components/memberPopup/panels/ActivitiesSubscribePanel.js.map +7 -0
- package/dist/cjs/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.d.ts +2 -1
- package/dist/cjs/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.js +1 -1
- package/dist/cjs/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.js.map +3 -3
- package/dist/cjs/components/memberPopup/panels/ConsumeCredits/index.js +1 -1
- package/dist/cjs/components/memberPopup/panels/ConsumeCredits/index.js.map +3 -3
- package/dist/cjs/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.d.ts +11 -0
- package/dist/cjs/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.js +2 -0
- package/dist/cjs/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.js.map +7 -0
- package/dist/cjs/components/memberPopup/panels/EarnCredits/SubscribeModal.js +1 -1
- package/dist/cjs/components/memberPopup/panels/EarnCredits/SubscribeModal.js.map +2 -2
- package/dist/cjs/components/memberPopup/panels/EarnCredits/index.js +1 -1
- package/dist/cjs/components/memberPopup/panels/EarnCredits/index.js.map +2 -2
- package/dist/cjs/components/memberPopup/panels/Referral/index.js +1 -1
- package/dist/cjs/components/memberPopup/panels/Referral/index.js.map +2 -2
- package/dist/cjs/components/memberPopup/type.d.ts +14 -2
- package/dist/cjs/components/memberPopup/type.js +1 -1
- package/dist/cjs/components/memberPopup/type.js.map +2 -2
- package/dist/esm/components/credits/context/hooks/useCountries.js +1 -1
- package/dist/esm/components/credits/context/hooks/useCountries.js.map +3 -3
- package/dist/esm/components/memberPopup/MemberPopup.js +1 -1
- package/dist/esm/components/memberPopup/MemberPopup.js.map +3 -3
- package/dist/esm/components/memberPopup/panels/ActivitiesSubscribePanel.d.ts +10 -0
- package/dist/esm/components/memberPopup/panels/ActivitiesSubscribePanel.js +2 -0
- package/dist/esm/components/memberPopup/panels/ActivitiesSubscribePanel.js.map +7 -0
- package/dist/esm/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.d.ts +2 -1
- package/dist/esm/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.js +1 -1
- package/dist/esm/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.js.map +3 -3
- package/dist/esm/components/memberPopup/panels/ConsumeCredits/index.js +1 -1
- package/dist/esm/components/memberPopup/panels/ConsumeCredits/index.js.map +3 -3
- package/dist/esm/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.d.ts +11 -0
- package/dist/esm/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.js +2 -0
- package/dist/esm/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.js.map +7 -0
- package/dist/esm/components/memberPopup/panels/EarnCredits/SubscribeModal.js +1 -1
- package/dist/esm/components/memberPopup/panels/EarnCredits/SubscribeModal.js.map +2 -2
- package/dist/esm/components/memberPopup/panels/EarnCredits/index.js +1 -1
- package/dist/esm/components/memberPopup/panels/EarnCredits/index.js.map +2 -2
- package/dist/esm/components/memberPopup/panels/Referral/index.js +1 -1
- package/dist/esm/components/memberPopup/panels/Referral/index.js.map +2 -2
- package/dist/esm/components/memberPopup/type.d.ts +14 -2
- package/dist/esm/components/memberPopup/type.js +1 -1
- package/dist/esm/components/memberPopup/type.js.map +2 -2
- package/package.json +1 -1
- package/src/components/credits/context/hooks/useCountries.ts +1 -2
- package/src/components/memberPopup/MemberPopup.tsx +18 -13
- package/src/components/memberPopup/panels/ActivitiesSubscribePanel.tsx +194 -0
- package/src/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.tsx +61 -28
- package/src/components/memberPopup/panels/ConsumeCredits/index.tsx +40 -14
- package/src/components/memberPopup/panels/EarnCredits/index.tsx +94 -64
- package/src/components/memberPopup/panels/Referral/index.tsx +1 -1
- package/src/components/memberPopup/type.ts +10 -1
- package/src/components/memberPopup/panels/ConsumeCredits/RedeemCouponModal.tsx +0 -150
- package/src/components/memberPopup/panels/EarnCredits/SubscribeModal.tsx +0 -144
- package/src/components/memberPopup/panels/EarnCredits/SubscribePanel.tsx +0 -147
|
@@ -84,7 +84,7 @@ function Referral({ copy, onClose }: Props) {
|
|
|
84
84
|
<Picture
|
|
85
85
|
source={copy.image.url}
|
|
86
86
|
alt={copy.image.alt ?? ''}
|
|
87
|
-
className={cn('h-[100px] w-full object-cover', rounded ? 'rounded-[8px]' : 'rounded-none')}
|
|
87
|
+
className={cn('mt-[8px] h-[100px] w-full object-cover', rounded ? 'rounded-[8px]' : 'rounded-none')}
|
|
88
88
|
/>
|
|
89
89
|
)}
|
|
90
90
|
|
|
@@ -37,6 +37,8 @@ export enum EarnCreditsTaskType {
|
|
|
37
37
|
export enum ConsumeCreditsItemType {
|
|
38
38
|
Coupon = 'COUPON',
|
|
39
39
|
RedeemAndBuy = 'REDEEM_AND_BUY',
|
|
40
|
+
GiftCard = 'GIFT_CARD',
|
|
41
|
+
Product = 'PRODUCT',
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
// ─── 通用 link(统一一种类型) ─────────────────────────
|
|
@@ -81,7 +83,7 @@ export type MemberPopupCopy = {
|
|
|
81
83
|
shopLabel: string
|
|
82
84
|
shopUrl?: string
|
|
83
85
|
}
|
|
84
|
-
|
|
86
|
+
activitiesSubscribe?: SubscribeModalCopy
|
|
85
87
|
earnCredits?: EarnCreditsCopy
|
|
86
88
|
consumeCredits?: ConsumeCreditsopy
|
|
87
89
|
referral?: ReferralCopy
|
|
@@ -141,6 +143,13 @@ export type SubscribeModalCopy = {
|
|
|
141
143
|
single_brand_subscribe?: boolean
|
|
142
144
|
genre?: string
|
|
143
145
|
sub_brand_type?: string
|
|
146
|
+
promo?: {
|
|
147
|
+
title?: string
|
|
148
|
+
description?: string
|
|
149
|
+
learnMoreLabel?: string
|
|
150
|
+
learnMoreUrl?: string
|
|
151
|
+
image?: { url: string; alt?: string }
|
|
152
|
+
}
|
|
144
153
|
}
|
|
145
154
|
|
|
146
155
|
// ─── ConsumeCredits Copy ────────────────────────────
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
-
import { Button, Text } from '@anker-in/headless-ui'
|
|
3
|
-
import { classNames as cn } from '@anker-in/lib'
|
|
4
|
-
import { CreditsModalContainer } from '../../../credits/modal/ModalContainer'
|
|
5
|
-
import { AlpcErrorCode } from '../../../credits/context/const'
|
|
6
|
-
import { useRedeemCoupon } from './hooks/useRedeemCoupon'
|
|
7
|
-
import useCopy from '../../../credits/context/hooks/useCopy'
|
|
8
|
-
import type { RedeemCouponModalCopy, AlpcConfig } from '../../type'
|
|
9
|
-
|
|
10
|
-
type Props = {
|
|
11
|
-
isOpen: boolean
|
|
12
|
-
onClose: () => void
|
|
13
|
-
itemTitle: string
|
|
14
|
-
itemCredits: number
|
|
15
|
-
itemImage?: { url: string; alt?: string }
|
|
16
|
-
redeemId: number
|
|
17
|
-
userId?: string
|
|
18
|
-
copy: RedeemCouponModalCopy
|
|
19
|
-
pointUnit?: string
|
|
20
|
-
alpcConfig: AlpcConfig
|
|
21
|
-
onError?: (code: number) => void
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function RedeemCouponModal({
|
|
25
|
-
isOpen,
|
|
26
|
-
onClose,
|
|
27
|
-
itemTitle,
|
|
28
|
-
itemCredits,
|
|
29
|
-
itemImage,
|
|
30
|
-
redeemId,
|
|
31
|
-
userId,
|
|
32
|
-
copy,
|
|
33
|
-
pointUnit = 'pts',
|
|
34
|
-
alpcConfig,
|
|
35
|
-
onError,
|
|
36
|
-
}: Props) {
|
|
37
|
-
const [couponCode, setCouponCode] = useState<string>()
|
|
38
|
-
const [errorInfo, setErrorInfo] = useState('')
|
|
39
|
-
const [disabled, setDisabled] = useState(false)
|
|
40
|
-
const { copy: copyText, copied } = useCopy()
|
|
41
|
-
|
|
42
|
-
const { isMutating: loading, trigger } = useRedeemCoupon(alpcConfig)
|
|
43
|
-
|
|
44
|
-
const handleConfirm = useCallback(async () => {
|
|
45
|
-
setErrorInfo('')
|
|
46
|
-
const { data } = await trigger({ user_id: userId, rule_id: redeemId })
|
|
47
|
-
if (!data) {
|
|
48
|
-
setErrorInfo(copy.commonError)
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
if (data.data?.success) {
|
|
52
|
-
setCouponCode(data.data.coupon_code)
|
|
53
|
-
} else {
|
|
54
|
-
let msg = copy.commonError
|
|
55
|
-
if (data.code === AlpcErrorCode.CodeLpcShopifyCouponRuleRedeemLimit) {
|
|
56
|
-
msg = copy.redeemLimitError
|
|
57
|
-
setDisabled(true)
|
|
58
|
-
} else if (data.code === AlpcErrorCode.CodeLpcRuleInventoryNotEnough) {
|
|
59
|
-
msg = copy.inventoryNotEnough
|
|
60
|
-
setDisabled(true)
|
|
61
|
-
} else if (data.code === AlpcErrorCode.CodeLpcNotEnoughCredits) {
|
|
62
|
-
msg = copy.creditsNotEnough
|
|
63
|
-
setDisabled(true)
|
|
64
|
-
} else if (data.code === AlpcErrorCode.CodeCrossSiteError) {
|
|
65
|
-
msg = copy.crossSiteError
|
|
66
|
-
setDisabled(true)
|
|
67
|
-
}
|
|
68
|
-
setErrorInfo(msg)
|
|
69
|
-
onError?.(data.code)
|
|
70
|
-
}
|
|
71
|
-
}, [trigger, userId, redeemId, copy, onError])
|
|
72
|
-
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
if (!isOpen) {
|
|
75
|
-
setCouponCode(undefined)
|
|
76
|
-
setErrorInfo('')
|
|
77
|
-
setDisabled(false)
|
|
78
|
-
}
|
|
79
|
-
}, [isOpen])
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
<CreditsModalContainer
|
|
83
|
-
isOpen={isOpen}
|
|
84
|
-
onClose={onClose}
|
|
85
|
-
className="w-[540px] md:h-auto"
|
|
86
|
-
scrollClassName="mb-[40px] px-0 min-l:px-0 md:mb-[24px]"
|
|
87
|
-
titleClassName="h-[56px] bg-[#f5f5f7]"
|
|
88
|
-
>
|
|
89
|
-
<div className="bg-[#f5f5f7] px-[48px] pb-[24px] md:px-[16px]">
|
|
90
|
-
{itemImage?.url && (
|
|
91
|
-
<div className="relative mx-auto h-[164px] w-[324px]">
|
|
92
|
-
<img src={itemImage.url} alt={itemImage.alt ?? ''} className="h-full w-full object-cover" />
|
|
93
|
-
</div>
|
|
94
|
-
)}
|
|
95
|
-
<p className={cn('mt-[30px] text-center text-[24px] font-bold md:mt-[18px] md:text-[20px]')}>
|
|
96
|
-
{itemCredits} {pointUnit}
|
|
97
|
-
</p>
|
|
98
|
-
</div>
|
|
99
|
-
|
|
100
|
-
<div className="bg-white px-[48px] pt-[24px] text-center md:px-[16px]">
|
|
101
|
-
{!couponCode ? (
|
|
102
|
-
<>
|
|
103
|
-
<Text className="block text-[24px] font-bold md:text-[20px]" html={copy.confirmTitle} />
|
|
104
|
-
<Button
|
|
105
|
-
loading={loading}
|
|
106
|
-
variant="primary"
|
|
107
|
-
size="lg"
|
|
108
|
-
className="mt-[16px] w-full"
|
|
109
|
-
onClick={handleConfirm}
|
|
110
|
-
disabled={disabled}
|
|
111
|
-
>
|
|
112
|
-
{copy.confirmButton}
|
|
113
|
-
</Button>
|
|
114
|
-
</>
|
|
115
|
-
) : (
|
|
116
|
-
<>
|
|
117
|
-
<div className="flex items-center justify-center gap-[10px]">
|
|
118
|
-
<span className="text-[24px] font-bold">{couponCode}</span>
|
|
119
|
-
<button
|
|
120
|
-
className={cn('text-[16px] font-bold underline', copied && 'pointer-events-none')}
|
|
121
|
-
onClick={() => copyText(couponCode)}
|
|
122
|
-
>
|
|
123
|
-
{copied ? 'Copied' : 'Copy'}
|
|
124
|
-
</button>
|
|
125
|
-
</div>
|
|
126
|
-
<Text className="mb-[4px] mt-[16px] text-[24px] font-bold md:text-[20px]" html={copy.successTitle} />
|
|
127
|
-
<Text className="text-[16px] font-bold text-[#6d6d6f] md:text-[14px]" html={copy.successDesc} />
|
|
128
|
-
<Button
|
|
129
|
-
variant="primary"
|
|
130
|
-
size="lg"
|
|
131
|
-
className="mt-[16px] w-full"
|
|
132
|
-
onClick={() => {
|
|
133
|
-
if (copy.successButtonUrl) window.location.href = copy.successButtonUrl
|
|
134
|
-
onClose()
|
|
135
|
-
}}
|
|
136
|
-
>
|
|
137
|
-
{copy.successButton}
|
|
138
|
-
</Button>
|
|
139
|
-
</>
|
|
140
|
-
)}
|
|
141
|
-
</div>
|
|
142
|
-
|
|
143
|
-
{errorInfo && (
|
|
144
|
-
<div className="bg-white px-[48px] py-[12px] text-center md:px-[16px]">
|
|
145
|
-
<Text className="text-[14px] font-semibold text-[#ff0000]" html={errorInfo} />
|
|
146
|
-
</div>
|
|
147
|
-
)}
|
|
148
|
-
</CreditsModalContainer>
|
|
149
|
-
)
|
|
150
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
-
import { Button, Checkbox, Text } from '@anker-in/headless-ui'
|
|
3
|
-
import { classNames as cn, useHeadlessContext } from '@anker-in/lib'
|
|
4
|
-
import Cookies from 'js-cookie'
|
|
5
|
-
import { parse } from 'query-string'
|
|
6
|
-
import { CreditsModalContainer } from '../../../credits/modal/ModalContainer'
|
|
7
|
-
import { emailValidate } from '../../../credits/context/utils'
|
|
8
|
-
import { useMemberPopupContext } from '../../context/provider'
|
|
9
|
-
import type { SubscribeModalCopy } from '../../type'
|
|
10
|
-
import { ROUNDED_BRANDS } from '../../../../constants'
|
|
11
|
-
|
|
12
|
-
type Props = {
|
|
13
|
-
isOpen: boolean
|
|
14
|
-
onClose: () => void
|
|
15
|
-
onSuccess?: () => void
|
|
16
|
-
copy: SubscribeModalCopy
|
|
17
|
-
email?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getAdCookie() {
|
|
21
|
-
if (typeof window === 'undefined') return { register_source: '' }
|
|
22
|
-
const { search, href } = window.location
|
|
23
|
-
const query = parse(search)
|
|
24
|
-
return {
|
|
25
|
-
fbuy_ref_code: Cookies.get('fbuy_ref_code'),
|
|
26
|
-
ref: Cookies.get('ref_ads'),
|
|
27
|
-
inviter_code: query.ic ?? query.inviter_code ?? Cookies.get('inviter_code'),
|
|
28
|
-
register_source: query.redirect ?? Cookies.get('reg_source') ?? href,
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function getRecaptchaToken(sitekey: string): Promise<string> {
|
|
33
|
-
if (typeof window === 'undefined' || !window?.grecaptcha?.execute) return ''
|
|
34
|
-
if (!sitekey) return ''
|
|
35
|
-
try {
|
|
36
|
-
return await window.grecaptcha.execute(sitekey, { action: 'activity' })
|
|
37
|
-
} catch {
|
|
38
|
-
return ''
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function EarnCreditsSubscribeModal({ isOpen, onClose, onSuccess, copy, email: initialEmail }: Props) {
|
|
43
|
-
const { alpcConfig } = useMemberPopupContext()
|
|
44
|
-
const { locale } = alpcConfig
|
|
45
|
-
const { brand: headlessBrand, storeDomain, recaptchaSitekey } = useHeadlessContext()
|
|
46
|
-
const rounded = ROUNDED_BRANDS.includes(headlessBrand)
|
|
47
|
-
const [policy, setPolicy] = useState(false)
|
|
48
|
-
const [email, setEmail] = useState(initialEmail ?? '')
|
|
49
|
-
const [errorMessage, setErrorMessage] = useState('')
|
|
50
|
-
const [successMessage, setSuccessMessage] = useState('')
|
|
51
|
-
const [loading, setLoading] = useState(false)
|
|
52
|
-
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (initialEmail) setEmail(initialEmail)
|
|
55
|
-
}, [initialEmail])
|
|
56
|
-
|
|
57
|
-
const handleSubmit = useCallback(async () => {
|
|
58
|
-
setErrorMessage('')
|
|
59
|
-
if (!email) return setErrorMessage('Please fill in your email')
|
|
60
|
-
if (!emailValidate(email)) return setErrorMessage('Invalid email address')
|
|
61
|
-
if (!policy) return setErrorMessage('Please agree to the policy')
|
|
62
|
-
|
|
63
|
-
setLoading(true)
|
|
64
|
-
const { register_source } = getAdCookie()
|
|
65
|
-
const recaptchaToken = await getRecaptchaToken(recaptchaSitekey || '')
|
|
66
|
-
try {
|
|
67
|
-
const response = await fetch('/api/multipass/rainbowbridge/activities', {
|
|
68
|
-
method: 'POST',
|
|
69
|
-
headers: {
|
|
70
|
-
'Content-Type': 'application/json',
|
|
71
|
-
'current-language': locale,
|
|
72
|
-
'X-Recaptcha-Token': recaptchaToken,
|
|
73
|
-
},
|
|
74
|
-
body: JSON.stringify({
|
|
75
|
-
email,
|
|
76
|
-
register_source,
|
|
77
|
-
shopify_domain: storeDomain,
|
|
78
|
-
single_brand_subscribe: copy.single_brand_subscribe ?? true,
|
|
79
|
-
genre: copy.genre ?? copy.dealsType ?? '',
|
|
80
|
-
sub_brand_type: copy.sub_brand_type ?? '',
|
|
81
|
-
}),
|
|
82
|
-
})
|
|
83
|
-
const result = await response.json()
|
|
84
|
-
if (!result.errors) {
|
|
85
|
-
setSuccessMessage(copy.successTips ?? 'Subscribed successfully!')
|
|
86
|
-
onSuccess?.()
|
|
87
|
-
setTimeout(() => {
|
|
88
|
-
setSuccessMessage('')
|
|
89
|
-
onClose()
|
|
90
|
-
}, 3000)
|
|
91
|
-
} else {
|
|
92
|
-
setErrorMessage(result.errors ?? 'Error occurred')
|
|
93
|
-
}
|
|
94
|
-
} catch {
|
|
95
|
-
setErrorMessage('Network error')
|
|
96
|
-
} finally {
|
|
97
|
-
setLoading(false)
|
|
98
|
-
}
|
|
99
|
-
}, [email, policy, locale, storeDomain, copy, onSuccess, onClose])
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<CreditsModalContainer
|
|
103
|
-
isOpen={isOpen}
|
|
104
|
-
onClose={onClose}
|
|
105
|
-
className="w-[540px] md:h-auto"
|
|
106
|
-
scrollClassName="mb-[40px] md:mb-[24px]"
|
|
107
|
-
useAnimation
|
|
108
|
-
animationClassName="md:translate-y-[100vh]"
|
|
109
|
-
>
|
|
110
|
-
<div className="flex flex-col gap-[16px] text-center">
|
|
111
|
-
<Text className="text-[22px] font-bold" html={copy.title} />
|
|
112
|
-
{copy.desc && (
|
|
113
|
-
<Text className="text-left text-[16px] font-semibold text-[#1f2021]" html={copy.desc} />
|
|
114
|
-
)}
|
|
115
|
-
<div className={cn('flex h-[52px] w-full overflow-hidden md:flex-col md:h-auto md:gap-[16px]', rounded ? 'rounded-[2px]' : 'rounded-none')}>
|
|
116
|
-
<input
|
|
117
|
-
className={cn('h-full flex-1 border border-[#d8d8d8] px-[14px] text-[16px] outline-none placeholder:text-[#999] md:h-[52px]', rounded ? 'rounded-l-[2px] md:rounded-[2px]' : 'rounded-none')}
|
|
118
|
-
placeholder={copy.placeholder}
|
|
119
|
-
value={email}
|
|
120
|
-
onChange={e => { setErrorMessage(''); setEmail(e.target.value.trim()) }}
|
|
121
|
-
/>
|
|
122
|
-
<Button
|
|
123
|
-
variant="primary"
|
|
124
|
-
loading={loading}
|
|
125
|
-
className={cn('!h-full !min-w-0 !px-[24px] md:!h-[46px]', rounded ? '!rounded-l-none !rounded-r-[2px] md:!rounded-[2px]' : '!rounded-none')}
|
|
126
|
-
onClick={handleSubmit}
|
|
127
|
-
>
|
|
128
|
-
{copy.buttonLabel ?? 'Subscribe'}
|
|
129
|
-
</Button>
|
|
130
|
-
</div>
|
|
131
|
-
<div className="flex w-full items-start gap-[8px]">
|
|
132
|
-
<Checkbox checked={policy} onCheckedChange={() => setPolicy(v => !v)} className="border-[#1d1d1f]" />
|
|
133
|
-
<label
|
|
134
|
-
className="text-left text-[14px] font-semibold text-[#777] [&_a]:underline"
|
|
135
|
-
dangerouslySetInnerHTML={{ __html: copy.policy ?? '' }}
|
|
136
|
-
/>
|
|
137
|
-
</div>
|
|
138
|
-
<div className={cn('text-left text-[16px] font-semibold', errorMessage && 'text-[#f84d4f]', successMessage && 'text-[#52c41a]')}>
|
|
139
|
-
<Text html={successMessage || errorMessage || ''} />
|
|
140
|
-
</div>
|
|
141
|
-
</div>
|
|
142
|
-
</CreditsModalContainer>
|
|
143
|
-
)
|
|
144
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
-
import { Checkbox, Text } from '@anker-in/headless-ui'
|
|
3
|
-
import { classNames as cn, useHeadlessContext } from '@anker-in/lib'
|
|
4
|
-
import Cookies from 'js-cookie'
|
|
5
|
-
import { parse } from 'query-string'
|
|
6
|
-
import { emailValidate } from '../../../credits/context/utils'
|
|
7
|
-
import { useMemberPopupContext } from '../../context/provider'
|
|
8
|
-
import type { SubscribeModalCopy } from '../../type'
|
|
9
|
-
import { ROUNDED_BRANDS } from '../../../../constants'
|
|
10
|
-
|
|
11
|
-
type Props = {
|
|
12
|
-
copy: SubscribeModalCopy
|
|
13
|
-
email?: string
|
|
14
|
-
onClose: () => void
|
|
15
|
-
onSuccess?: () => void
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getAdCookie() {
|
|
19
|
-
if (typeof window === 'undefined') return { register_source: '' }
|
|
20
|
-
const { search, href } = window.location
|
|
21
|
-
const query = parse(search)
|
|
22
|
-
return {
|
|
23
|
-
fbuy_ref_code: Cookies.get('fbuy_ref_code'),
|
|
24
|
-
ref: Cookies.get('ref_ads'),
|
|
25
|
-
inviter_code: query.ic ?? query.inviter_code ?? Cookies.get('inviter_code'),
|
|
26
|
-
register_source: query.redirect ?? Cookies.get('reg_source') ?? href,
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function getRecaptchaToken(sitekey: string): Promise<string> {
|
|
31
|
-
if (typeof window === 'undefined' || !window?.grecaptcha?.execute) return ''
|
|
32
|
-
if (!sitekey) return ''
|
|
33
|
-
try {
|
|
34
|
-
return await window.grecaptcha.execute(sitekey, { action: 'activity' })
|
|
35
|
-
} catch {
|
|
36
|
-
return ''
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export default function SubscribePanel({ copy, email: initialEmail, onClose, onSuccess }: Props) {
|
|
41
|
-
const { alpcConfig } = useMemberPopupContext()
|
|
42
|
-
const { locale } = alpcConfig
|
|
43
|
-
const { brand: headlessBrand, storeDomain, recaptchaSitekey } = useHeadlessContext()
|
|
44
|
-
const rounded = ROUNDED_BRANDS.includes(headlessBrand)
|
|
45
|
-
const [policy, setPolicy] = useState(false)
|
|
46
|
-
const [email, setEmail] = useState(initialEmail ?? '')
|
|
47
|
-
const [errorMessage, setErrorMessage] = useState('')
|
|
48
|
-
const [successMessage, setSuccessMessage] = useState('')
|
|
49
|
-
const [loading, setLoading] = useState(false)
|
|
50
|
-
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
if (initialEmail) setEmail(initialEmail)
|
|
53
|
-
}, [initialEmail])
|
|
54
|
-
|
|
55
|
-
const handleSubmit = useCallback(async () => {
|
|
56
|
-
setErrorMessage('')
|
|
57
|
-
if (!email) return setErrorMessage('Please fill in your email')
|
|
58
|
-
if (!emailValidate(email)) return setErrorMessage('Invalid email address')
|
|
59
|
-
if (!policy) return setErrorMessage('Please agree to the policy')
|
|
60
|
-
|
|
61
|
-
setLoading(true)
|
|
62
|
-
const { register_source } = getAdCookie()
|
|
63
|
-
const recaptchaToken = await getRecaptchaToken(recaptchaSitekey || '')
|
|
64
|
-
try {
|
|
65
|
-
const response = await fetch('/api/multipass/rainbowbridge/activities', {
|
|
66
|
-
method: 'POST',
|
|
67
|
-
headers: {
|
|
68
|
-
'Content-Type': 'application/json',
|
|
69
|
-
'current-language': locale,
|
|
70
|
-
'X-Recaptcha-Token': recaptchaToken,
|
|
71
|
-
},
|
|
72
|
-
body: JSON.stringify({
|
|
73
|
-
email,
|
|
74
|
-
register_source,
|
|
75
|
-
shopify_domain: storeDomain,
|
|
76
|
-
single_brand_subscribe: copy.single_brand_subscribe ?? true,
|
|
77
|
-
genre: copy.genre ?? copy.dealsType ?? '',
|
|
78
|
-
sub_brand_type: copy.sub_brand_type ?? '',
|
|
79
|
-
}),
|
|
80
|
-
})
|
|
81
|
-
const result = await response.json()
|
|
82
|
-
if (!result.errors) {
|
|
83
|
-
setSuccessMessage(copy.successTips ?? 'Subscribed successfully!')
|
|
84
|
-
onSuccess?.()
|
|
85
|
-
setTimeout(() => {
|
|
86
|
-
setSuccessMessage('')
|
|
87
|
-
onClose()
|
|
88
|
-
}, 3000)
|
|
89
|
-
} else {
|
|
90
|
-
setErrorMessage(result.errors ?? 'Error occurred')
|
|
91
|
-
}
|
|
92
|
-
} catch {
|
|
93
|
-
setErrorMessage('Network error')
|
|
94
|
-
} finally {
|
|
95
|
-
setLoading(false)
|
|
96
|
-
}
|
|
97
|
-
}, [email, policy, locale, storeDomain, copy, onClose, onSuccess])
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<div className="flex flex-col gap-[12px]">
|
|
101
|
-
<div className="flex flex-col gap-[8px]">
|
|
102
|
-
{copy.title && (
|
|
103
|
-
<h2 className="text-[24px] font-[700] leading-[1.2] text-[#080a0f]">{copy.title}</h2>
|
|
104
|
-
)}
|
|
105
|
-
{copy.desc && (
|
|
106
|
-
<Text className="text-[16px] text-[#1e2024]" html={copy.desc} />
|
|
107
|
-
)}
|
|
108
|
-
</div>
|
|
109
|
-
|
|
110
|
-
<div className="flex flex-col gap-[16px]">
|
|
111
|
-
<input
|
|
112
|
-
className={cn(
|
|
113
|
-
'h-[46px] w-full border px-[14px] text-[16px] outline-none placeholder:text-[#999]',
|
|
114
|
-
errorMessage ? 'border-[#f84d4f]' : 'border-[#d8d8d8]',
|
|
115
|
-
rounded ? 'rounded-[4px]' : 'rounded-none'
|
|
116
|
-
)}
|
|
117
|
-
placeholder={copy.placeholder}
|
|
118
|
-
value={email}
|
|
119
|
-
onChange={e => { setErrorMessage(''); setEmail(e.target.value.trim()) }}
|
|
120
|
-
/>
|
|
121
|
-
|
|
122
|
-
<div className="flex w-full items-start gap-[8px]">
|
|
123
|
-
<Checkbox checked={policy} onCheckedChange={() => setPolicy(v => !v)} className="mt-[2px] border-[#1d1d1f]" />
|
|
124
|
-
<label
|
|
125
|
-
className="cursor-pointer text-left text-[14px] text-[#1e2024] [&_a]:underline"
|
|
126
|
-
onClick={() => setPolicy(v => !v)}
|
|
127
|
-
dangerouslySetInnerHTML={{ __html: copy.policy ?? '' }}
|
|
128
|
-
/>
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
{(errorMessage || successMessage) && (
|
|
132
|
-
<div className={cn('text-[14px] font-[700]', errorMessage && 'text-[#f84d4f]', successMessage && 'text-[#52c41a]')}>
|
|
133
|
-
<Text html={successMessage || errorMessage} />
|
|
134
|
-
</div>
|
|
135
|
-
)}
|
|
136
|
-
|
|
137
|
-
<button
|
|
138
|
-
className={cn('w-full cursor-pointer bg-[#080a0f] py-[14px] text-[14px] font-[700] text-white disabled:opacity-60', rounded ? 'rounded-[4px]' : 'rounded-none')}
|
|
139
|
-
disabled={loading}
|
|
140
|
-
onClick={handleSubmit}
|
|
141
|
-
>
|
|
142
|
-
{copy.buttonLabel ?? 'Subscribe'}
|
|
143
|
-
</button>
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
)
|
|
147
|
-
}
|