@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.
Files changed (60) hide show
  1. package/dist/cjs/components/credits/context/hooks/useCountries.js +1 -1
  2. package/dist/cjs/components/credits/context/hooks/useCountries.js.map +3 -3
  3. package/dist/cjs/components/memberPopup/MemberPopup.js +1 -1
  4. package/dist/cjs/components/memberPopup/MemberPopup.js.map +3 -3
  5. package/dist/cjs/components/memberPopup/panels/ActivitiesSubscribePanel.d.ts +10 -0
  6. package/dist/cjs/components/memberPopup/panels/ActivitiesSubscribePanel.js +2 -0
  7. package/dist/cjs/components/memberPopup/panels/ActivitiesSubscribePanel.js.map +7 -0
  8. package/dist/cjs/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.d.ts +2 -1
  9. package/dist/cjs/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.js +1 -1
  10. package/dist/cjs/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.js.map +3 -3
  11. package/dist/cjs/components/memberPopup/panels/ConsumeCredits/index.js +1 -1
  12. package/dist/cjs/components/memberPopup/panels/ConsumeCredits/index.js.map +3 -3
  13. package/dist/cjs/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.d.ts +11 -0
  14. package/dist/cjs/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.js +2 -0
  15. package/dist/cjs/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.js.map +7 -0
  16. package/dist/cjs/components/memberPopup/panels/EarnCredits/SubscribeModal.js +1 -1
  17. package/dist/cjs/components/memberPopup/panels/EarnCredits/SubscribeModal.js.map +2 -2
  18. package/dist/cjs/components/memberPopup/panels/EarnCredits/index.js +1 -1
  19. package/dist/cjs/components/memberPopup/panels/EarnCredits/index.js.map +2 -2
  20. package/dist/cjs/components/memberPopup/panels/Referral/index.js +1 -1
  21. package/dist/cjs/components/memberPopup/panels/Referral/index.js.map +2 -2
  22. package/dist/cjs/components/memberPopup/type.d.ts +14 -2
  23. package/dist/cjs/components/memberPopup/type.js +1 -1
  24. package/dist/cjs/components/memberPopup/type.js.map +2 -2
  25. package/dist/esm/components/credits/context/hooks/useCountries.js +1 -1
  26. package/dist/esm/components/credits/context/hooks/useCountries.js.map +3 -3
  27. package/dist/esm/components/memberPopup/MemberPopup.js +1 -1
  28. package/dist/esm/components/memberPopup/MemberPopup.js.map +3 -3
  29. package/dist/esm/components/memberPopup/panels/ActivitiesSubscribePanel.d.ts +10 -0
  30. package/dist/esm/components/memberPopup/panels/ActivitiesSubscribePanel.js +2 -0
  31. package/dist/esm/components/memberPopup/panels/ActivitiesSubscribePanel.js.map +7 -0
  32. package/dist/esm/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.d.ts +2 -1
  33. package/dist/esm/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.js +1 -1
  34. package/dist/esm/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.js.map +3 -3
  35. package/dist/esm/components/memberPopup/panels/ConsumeCredits/index.js +1 -1
  36. package/dist/esm/components/memberPopup/panels/ConsumeCredits/index.js.map +3 -3
  37. package/dist/esm/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.d.ts +11 -0
  38. package/dist/esm/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.js +2 -0
  39. package/dist/esm/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.js.map +7 -0
  40. package/dist/esm/components/memberPopup/panels/EarnCredits/SubscribeModal.js +1 -1
  41. package/dist/esm/components/memberPopup/panels/EarnCredits/SubscribeModal.js.map +2 -2
  42. package/dist/esm/components/memberPopup/panels/EarnCredits/index.js +1 -1
  43. package/dist/esm/components/memberPopup/panels/EarnCredits/index.js.map +2 -2
  44. package/dist/esm/components/memberPopup/panels/Referral/index.js +1 -1
  45. package/dist/esm/components/memberPopup/panels/Referral/index.js.map +2 -2
  46. package/dist/esm/components/memberPopup/type.d.ts +14 -2
  47. package/dist/esm/components/memberPopup/type.js +1 -1
  48. package/dist/esm/components/memberPopup/type.js.map +2 -2
  49. package/package.json +1 -1
  50. package/src/components/credits/context/hooks/useCountries.ts +1 -2
  51. package/src/components/memberPopup/MemberPopup.tsx +18 -13
  52. package/src/components/memberPopup/panels/ActivitiesSubscribePanel.tsx +194 -0
  53. package/src/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.tsx +61 -28
  54. package/src/components/memberPopup/panels/ConsumeCredits/index.tsx +40 -14
  55. package/src/components/memberPopup/panels/EarnCredits/index.tsx +94 -64
  56. package/src/components/memberPopup/panels/Referral/index.tsx +1 -1
  57. package/src/components/memberPopup/type.ts +10 -1
  58. package/src/components/memberPopup/panels/ConsumeCredits/RedeemCouponModal.tsx +0 -150
  59. package/src/components/memberPopup/panels/EarnCredits/SubscribeModal.tsx +0 -144
  60. package/src/components/memberPopup/panels/EarnCredits/SubscribePanel.tsx +0 -147
@@ -0,0 +1,194 @@
1
+ import { useCallback, useEffect, useState } from 'react'
2
+ import { Button, Checkbox, Picture, 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
+ onSuccess?: () => void
13
+ copy: SubscribeModalCopy
14
+ email?: string
15
+ hideForm: boolean
16
+ onHideForm: () => void
17
+ }
18
+
19
+ function getAdCookie() {
20
+ if (typeof window === 'undefined') return { register_source: '' }
21
+ const { search, href } = window.location
22
+ const query = parse(search)
23
+ return {
24
+ fbuy_ref_code: Cookies.get('fbuy_ref_code'),
25
+ ref: Cookies.get('ref_ads'),
26
+ inviter_code: query.ic ?? query.inviter_code ?? Cookies.get('inviter_code'),
27
+ register_source: query.redirect ?? Cookies.get('reg_source') ?? href,
28
+ }
29
+ }
30
+
31
+ async function getRecaptchaToken(sitekey: string): Promise<string> {
32
+ if (typeof window === 'undefined' || !window?.grecaptcha?.execute) return ''
33
+ if (!sitekey) return ''
34
+ try {
35
+ return await window.grecaptcha.execute(sitekey, { action: 'activity' })
36
+ } catch {
37
+ return ''
38
+ }
39
+ }
40
+
41
+ export default function ActivitiesSubscribeModal({
42
+ onSuccess,
43
+ copy,
44
+ email: initialEmail,
45
+ hideForm,
46
+ onHideForm,
47
+ }: Props) {
48
+ const { alpcConfig } = useMemberPopupContext()
49
+ const { locale } = alpcConfig
50
+ const { brand: headlessBrand, storeDomain, recaptchaSitekey } = useHeadlessContext()
51
+ const rounded = ROUNDED_BRANDS.includes(headlessBrand)
52
+ const [policy, setPolicy] = useState(false)
53
+ const [email, setEmail] = useState(initialEmail ?? '')
54
+ const [errorMessage, setErrorMessage] = useState('')
55
+ const [successMessage, setSuccessMessage] = useState('')
56
+ const [loading, setLoading] = useState(false)
57
+
58
+ useEffect(() => {
59
+ if (initialEmail) setEmail(initialEmail)
60
+ }, [initialEmail])
61
+
62
+ const handleSubmit = useCallback(async () => {
63
+ setErrorMessage('')
64
+ if (!email) return setErrorMessage('Please fill in your email')
65
+ if (!emailValidate(email)) return setErrorMessage('Invalid email address')
66
+ if (!policy) return setErrorMessage('Please agree to the policy')
67
+
68
+ setLoading(true)
69
+ const { register_source } = getAdCookie()
70
+ const recaptchaToken = await getRecaptchaToken(recaptchaSitekey || '')
71
+ try {
72
+ const response = await fetch('/api/multipass/rainbowbridge/activities', {
73
+ method: 'POST',
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ 'current-language': locale,
77
+ 'X-Recaptcha-Token': recaptchaToken,
78
+ },
79
+ body: JSON.stringify({
80
+ email,
81
+ register_source,
82
+ shopify_domain: storeDomain,
83
+ single_brand_subscribe: copy.single_brand_subscribe ?? true,
84
+ genre: copy.genre ?? copy.dealsType ?? '',
85
+ sub_brand_type: copy.sub_brand_type ?? '',
86
+ }),
87
+ })
88
+ const result = await response.json()
89
+ if (!result.errors) {
90
+ setSuccessMessage(copy.successTips ?? 'Subscribed successfully!')
91
+ onSuccess?.()
92
+ setTimeout(() => {
93
+ onHideForm()
94
+ setSuccessMessage('')
95
+ }, 3000)
96
+ } else {
97
+ setErrorMessage(result.errors ?? 'Error occurred')
98
+ }
99
+ } catch {
100
+ setErrorMessage('Network error')
101
+ } finally {
102
+ setLoading(false)
103
+ }
104
+ }, [email, policy, locale, storeDomain, copy, onSuccess, onHideForm])
105
+
106
+ return (
107
+ <div className="flex flex-col gap-[16px] text-center">
108
+ {!hideForm && (
109
+ <>
110
+ <Text className="text-[22px] font-bold" html={copy.title} />
111
+ {copy.desc && <Text className="text-left text-[16px] font-semibold text-[#1f2021]" html={copy.desc} />}
112
+
113
+ <input
114
+ className={cn(
115
+ 'h-[46px] w-full border px-[14px] text-[16px] outline-none placeholder:text-[#999]',
116
+ errorMessage ? 'border-[#f84d4f]' : 'border-[#d8d8d8]',
117
+ rounded ? 'rounded-[4px]' : 'rounded-none'
118
+ )}
119
+ placeholder={copy.placeholder}
120
+ value={email}
121
+ onChange={e => {
122
+ setErrorMessage('')
123
+ setEmail(e.target.value.trim())
124
+ }}
125
+ />
126
+
127
+ <div className="flex w-full items-start gap-[8px]">
128
+ <Checkbox checked={policy} onCheckedChange={() => setPolicy(v => !v)} className="border-[#1d1d1f]" />
129
+ <label
130
+ className="text-left text-[14px] font-semibold text-[#777] [&_a]:underline"
131
+ dangerouslySetInnerHTML={{ __html: copy.policy ?? '' }}
132
+ />
133
+ </div>
134
+
135
+ <button
136
+ className={cn(
137
+ 'w-full cursor-pointer bg-[#080a0f] py-[14px] text-[14px] font-[700] text-white disabled:opacity-60',
138
+ rounded ? 'rounded-[4px]' : 'rounded-none'
139
+ )}
140
+ disabled={loading}
141
+ onClick={handleSubmit}
142
+ >
143
+ {copy.buttonLabel ?? 'Subscribe'}
144
+ </button>
145
+
146
+ {errorMessage && (
147
+ <div className="text-left text-[16px] font-semibold text-[#f84d4f]">
148
+ <Text html={errorMessage} />
149
+ </div>
150
+ )}
151
+ <div className="text-left text-[16px] font-semibold text-[#52c41a]">
152
+ <Text html={successMessage} />
153
+ </div>
154
+ </>
155
+ )}
156
+
157
+ {copy.promo && (
158
+ <div className="flex flex-col text-left">
159
+ <div className="flex flex-col gap-[8px]">
160
+ {copy.promo?.title && (
161
+ <h3 className="text-[20px] font-[700] leading-[1.2] text-[#080a0f]">{copy.promo?.title}</h3>
162
+ )}
163
+ {copy.promo.description && (
164
+ <p className="text-[14px] leading-[1.5] text-[#1e2024]">{copy.promo.description}</p>
165
+ )}
166
+ {copy.promo.learnMoreLabel && copy.promo.learnMoreUrl && (
167
+ <a
168
+ href={copy.promo.learnMoreUrl}
169
+ target="_blank"
170
+ rel="noopener noreferrer"
171
+ className="flex items-center gap-[4px] text-[14px] font-[700] text-[#080a0f] underline"
172
+ >
173
+ {copy.promo.learnMoreLabel}
174
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
175
+ <path
176
+ d="M5.52827 3.52876C5.78862 3.26841 6.21063 3.26841 6.47098 3.52876L10.471 7.52876C10.7313 7.78911 10.7313 8.21112 10.471 8.47147L6.47098 12.4715C6.21063 12.7318 5.78862 12.7318 5.52827 12.4715C5.26792 12.2111 5.26792 11.7891 5.52827 11.5288L9.05692 8.00011L5.52827 4.47147C5.26792 4.21112 5.26792 3.78911 5.52827 3.52876Z"
177
+ fill="#080A0F"
178
+ />
179
+ </svg>
180
+ </a>
181
+ )}
182
+ </div>
183
+ {copy.promo.image?.url && (
184
+ <Picture
185
+ source={copy.promo.image.url}
186
+ alt={copy.promo.image.alt ?? ''}
187
+ className={cn('h-[100px] mt-[8px] w-full object-cover', rounded ? 'rounded-[8px]' : 'rounded-none')}
188
+ />
189
+ )}
190
+ </div>
191
+ )}
192
+ </div>
193
+ )
194
+ }
@@ -14,6 +14,7 @@ type Props = {
14
14
  itemImage?: { url: string; alt?: string }
15
15
  itemValue?: string
16
16
  itemOff?: string
17
+ itemRules?: string[]
17
18
  redeemId: number
18
19
  userId?: string
19
20
  copy: RedeemCouponModalCopy
@@ -29,6 +30,7 @@ export default function RedeemCouponPanel({
29
30
  itemImage,
30
31
  itemValue,
31
32
  itemOff,
33
+ itemRules,
32
34
  redeemId,
33
35
  userId,
34
36
  copy,
@@ -80,38 +82,45 @@ export default function RedeemCouponPanel({
80
82
  setDisabled(false)
81
83
  }, [redeemId])
82
84
 
85
+ const rulesSection = itemRules && itemRules.length > 0 && (
86
+ <div className="w-full text-[16px] font-bold leading-[1.4] text-[#6D6D6F] md:mt-[12px] md:text-[14px]">
87
+ <ul className="ml-4 grid grid-flow-row gap-[4px]">
88
+ {itemRules?.map((text, index) => (
89
+ <li className="list-disc" key={index}>
90
+ <Text html={text} className="font-bold" />
91
+ </li>
92
+ ))}
93
+ </ul>
94
+ </div>
95
+ )
96
+
83
97
  return (
84
- <div className="flex flex-col gap-[16px]">
98
+ <div className="flex flex-col">
85
99
  {itemImage?.url && (
86
- <div className={cn('relative overflow-hidden w-fit mx-auto', rounded ? 'rounded-[8px]' : 'rounded-none')}>
87
- <img src={itemImage.url} alt={itemImage.alt ?? ''} className="mx-auto h-[140px] object-contain" />
88
- {itemValue && (
89
- <div className="absolute bottom-[20px] left-[20px] text-[32px] font-[800] leading-none text-white">
90
- {itemValue}
91
- {itemOff ? ` ${itemOff}` : ''}
92
- </div>
93
- )}
100
+ <div className="relative w-full px-[24px]">
101
+ <div className={cn('relative mx-auto overflow-hidden', rounded ? 'rounded-[8px]' : 'rounded-none')}>
102
+ <img src={itemImage.url} alt={itemImage.alt ?? ''} className="mx-auto h-[140px] w-full object-cover" />
103
+ {itemValue && (
104
+ <div className="absolute bottom-[20px] left-[20px] text-[32px] font-[800] leading-none text-white">
105
+ {itemValue}
106
+ {itemOff ? ` ${itemOff}` : ''}
107
+ </div>
108
+ )}
109
+ </div>
94
110
  </div>
95
111
  )}
96
112
 
97
- <p className="text-center text-[24px] font-[700] text-[#1d1d1f]">
98
- {itemCredits} {pointUnit}
99
- </p>
100
-
101
- {!couponCode ? (
102
- <div className="flex flex-col gap-[12px] text-center">
103
- <Text className="block text-[20px] font-[700]" html={copy.confirmTitle} />
104
- <Button
105
- className={cn('w-full py-[12px] text-[14px] font-[700] text-white disabled:opacity-60 bg-[#080a0f]', rounded ? 'rounded-[4px]' : 'rounded-none')}
106
- disabled={disabled}
107
- loading={loading}
108
- onClick={handleConfirm}
109
- >
110
- {copy.confirmButton}
111
- </Button>
113
+ {!couponCode && (
114
+ <div className="flex flex-col gap-[16px] px-[24px] py-[20px]">
115
+ <p className="text-center text-[24px] font-[700] text-[#1d1d1f]">
116
+ {itemCredits} {pointUnit}
117
+ </p>
118
+ {rulesSection}
112
119
  </div>
113
- ) : (
114
- <div className="flex flex-col gap-[12px] text-center">
120
+ )}
121
+
122
+ {couponCode && (
123
+ <div className="flex flex-col gap-[8px] text-center">
115
124
  <div className="flex items-center justify-center gap-[10px]">
116
125
  <span className="text-[24px] font-[700]">{couponCode}</span>
117
126
  <button
@@ -125,7 +134,10 @@ export default function RedeemCouponPanel({
125
134
  <Text className="text-[20px] font-[700]" html={copy.successTitle} />
126
135
  <Text className="text-[14px] font-[700] text-[#6d6d6f]" html={copy.successDesc} />
127
136
  <Button
128
- className={cn('w-full py-[12px] text-[14px] font-[700] text-white bg-[#080a0f]', rounded ? 'rounded-[4px]' : 'rounded-none')}
137
+ className={cn(
138
+ 'w-full py-[12px] text-[14px] font-[700] text-white bg-[#080a0f]',
139
+ rounded ? 'rounded-[4px]' : 'rounded-none'
140
+ )}
129
141
  onClick={() => {
130
142
  if (copy.successButtonUrl) window.location.href = copy.successButtonUrl
131
143
  onClose()
@@ -136,7 +148,28 @@ export default function RedeemCouponPanel({
136
148
  </div>
137
149
  )}
138
150
 
139
- {errorInfo && <Text className="text-center text-[14px] font-[700] text-[#ff0000]" html={errorInfo} />}
151
+ {!couponCode && (
152
+ <div className="sticky bottom-0 bg-white border-t border-[#e5e5e5] pt-[10px]">
153
+ <div className="flex flex-col gap-[8px] text-center">
154
+ <Text className="block text-[16px] font-[700] leading-[1.3]" html={copy.confirmTitle} />
155
+ <Button
156
+ className={cn(
157
+ 'w-full py-[12px] text-[14px] font-[700] text-white disabled:opacity-60 bg-[#080a0f]',
158
+ rounded ? 'rounded-[4px]' : 'rounded-none'
159
+ )}
160
+ disabled={disabled}
161
+ loading={loading}
162
+ onClick={handleConfirm}
163
+ >
164
+ {copy.confirmButton}
165
+ </Button>
166
+ </div>
167
+
168
+ {errorInfo && (
169
+ <Text className="mt-[8px] text-center text-[14px] font-[700] text-[#ff0000]" html={errorInfo} />
170
+ )}
171
+ </div>
172
+ )}
140
173
  </div>
141
174
  )
142
175
  }
@@ -56,10 +56,14 @@ function ConsumeCredits({ copy, onClose }: Props) {
56
56
 
57
57
  let product
58
58
  let variant
59
- if (item.type === ConsumeCreditsItemType.RedeemAndBuy && item.product) {
59
+ if (
60
+ (item.type === ConsumeCreditsItemType.RedeemAndBuy ||
61
+ item.type === ConsumeCreditsItemType.Product) &&
62
+ item.product
63
+ ) {
60
64
  product = products?.find((p: any) => p.handle === item.product!.handle)
61
65
  variant = product?.variants?.find((v: any) => v.sku === item.product!.sku) ?? product?.variants?.[0]
62
- if (!product || !variant) return null
66
+ if (item.type === ConsumeCreditsItemType.RedeemAndBuy && (!product || !variant)) return null
63
67
  }
64
68
 
65
69
  return { ...item, alpcData, insufficient, soldOut, product, variant }
@@ -74,6 +78,12 @@ function ConsumeCredits({ copy, onClose }: Props) {
74
78
  }
75
79
 
76
80
  function isDisabled(item: (typeof list)[number]) {
81
+ if (
82
+ item.type === ConsumeCreditsItemType.GiftCard ||
83
+ item.type === ConsumeCreditsItemType.Product
84
+ ) {
85
+ return item.insufficient
86
+ }
77
87
  return item.soldOut || item.insufficient
78
88
  }
79
89
 
@@ -82,6 +92,19 @@ function ConsumeCredits({ copy, onClose }: Props) {
82
92
  openSignUpPopup()
83
93
  return
84
94
  }
95
+ if (!profile?.activated) {
96
+ pushView({ type: MemberPopupPanelType.Activate })
97
+ return
98
+ }
99
+ if (
100
+ item.type === ConsumeCreditsItemType.GiftCard ||
101
+ item.type === ConsumeCreditsItemType.Product
102
+ ) {
103
+ if (copy.creditsPage?.url) {
104
+ window.open(copy.creditsPage.url, '_blank', 'noopener,noreferrer')
105
+ }
106
+ return
107
+ }
85
108
  if (item.type === ConsumeCreditsItemType.Coupon) {
86
109
  pushView({
87
110
  type: MemberPopupPanelType.RedeemCoupon,
@@ -93,6 +116,7 @@ function ConsumeCredits({ copy, onClose }: Props) {
93
116
  : item.image,
94
117
  itemValue: item.value,
95
118
  itemOff: copy.off,
119
+ itemRules: item.rules,
96
120
  redeemId: Number(item.redeemId),
97
121
  userId: profile?.user_id,
98
122
  alpcConfig,
@@ -136,17 +160,18 @@ function ConsumeCredits({ copy, onClose }: Props) {
136
160
  </div>
137
161
  ) : (
138
162
  <div className="flex flex-col">
139
- {list.map((item, idx) => (
163
+ {list.map((item, idx) => {
164
+ const hasProduct = !!item.product
165
+ return (
140
166
  <div key={idx} className="relative py-[16px]">
141
167
  {idx > 0 && <div className="absolute inset-x-0 top-0 h-px bg-[#dadce0]" />}
142
168
  <div className="flex items-center gap-[12px]">
143
169
  {(() => {
144
- const isRedeemAndBuy = item.type === ConsumeCreditsItemType.RedeemAndBuy
145
170
  const mappedImgUrl = copy.imageMapping?.[item.type]?.url
146
- const imgUrl = isRedeemAndBuy
171
+ const imgUrl = hasProduct
147
172
  ? (item.variant?.image?.url ?? item.product?.images?.[0]?.url ?? item.image?.url)
148
173
  : (mappedImgUrl ?? item.image?.url)
149
- const imgAlt = isRedeemAndBuy
174
+ const imgAlt = hasProduct
150
175
  ? (item.variant?.image?.altText ?? item.product?.images?.[0]?.alt ?? item.image?.alt ?? '')
151
176
  : (item.image?.alt ?? '')
152
177
  if (!imgUrl) return null
@@ -156,9 +181,9 @@ function ConsumeCredits({ copy, onClose }: Props) {
156
181
  source={imgUrl}
157
182
  alt={imgAlt}
158
183
  className="flex size-full items-center justify-center"
159
- imgClassName={isRedeemAndBuy ? 'object-cover' : 'h-full w-auto object-contain'}
184
+ imgClassName={hasProduct ? 'object-cover' : 'h-full w-auto object-contain'}
160
185
  />
161
- {!isRedeemAndBuy && item.value && (
186
+ {!hasProduct && item.value && (
162
187
  <div className="absolute bottom-[20px] left-[4px] text-[14px] font-[800] leading-none text-white">
163
188
  {item.value} {copy.off}
164
189
  </div>
@@ -168,12 +193,12 @@ function ConsumeCredits({ copy, onClose }: Props) {
168
193
  })()}
169
194
  <div className="min-w-0 flex-1">
170
195
  <div className="text-[16px] font-[700] text-[#1e2024] line-clamp-2">
171
- {item.type === ConsumeCreditsItemType.RedeemAndBuy && item.product?.title
196
+ {hasProduct && item.product?.title
172
197
  ? item.product.title
173
198
  : item.title}
174
199
  </div>
175
200
  <div className="mt-[4px] text-[14px] text-[#666]">
176
- {item.type === ConsumeCreditsItemType.RedeemAndBuy && item.price != null
201
+ {hasProduct && item.price != null
177
202
  ? `${numberFormat(item.alpcData.consume_credits)} ${copy.pointUnit ?? ''} + $${Number(item.price).toFixed(2)}`
178
203
  : `${numberFormat(item.alpcData.consume_credits)} ${copy.pointUnit ?? ''}`}
179
204
  </div>
@@ -188,17 +213,18 @@ function ConsumeCredits({ copy, onClose }: Props) {
188
213
  {getButtonLabel(item)}
189
214
  </Button>
190
215
  {item.insufficient && (
191
- <div className="pointer-events-none absolute left-[48px] top-full mt-[10px] hidden -translate-x-1/2 group-hover:block">
192
- <div className="relative bg-[#6D6D6F] p-[16px] text-[14px] font-[700] text-white shadow md:px-[10px] md:py-[8px]">
216
+ <div className="pointer-events-none absolute right-0 top-full z-[100] mt-[10px] hidden w-[200px] group-hover:block">
217
+ <div className="relative bg-[#6D6D6F] p-[16px] text-[14px] font-[700] text-white shadow">
193
218
  <p className="text-pretty tracking-[-0.02em] text-[14px]">{copy.insufficientCredits}</p>
194
- <div className="absolute -top-[8px] left-[48px] size-[16px] origin-top-left rotate-45 rounded-[2px] bg-[#6D6D6F]" />
219
+ <div className="absolute -top-[8px] right-[20px] size-[16px] origin-top-left rotate-45 rounded-[2px] bg-[#6D6D6F]" />
195
220
  </div>
196
221
  </div>
197
222
  )}
198
223
  </div>
199
224
  </div>
200
225
  </div>
201
- ))}
226
+ )
227
+ })}
202
228
  </div>
203
229
  )}
204
230
 
@@ -48,66 +48,90 @@ function EarnCredits({
48
48
  const { brand } = useHeadlessContext()
49
49
  const rounded = ROUNDED_BRANDS.includes(brand)
50
50
 
51
+ console.log('creditInfo', creditInfo)
52
+
51
53
  const isLogin = !!profile
52
54
 
53
55
  const isSubscribed = useSubscribed({
54
56
  userId: profile?.user_id,
55
- brand: alpcConfig.brand,
57
+ brand: brand,
56
58
  email: profile?.email,
57
59
  locale,
58
60
  apiBaseUrl: alpcConfig.apiBaseUrl,
59
61
  })
60
62
 
61
63
  const profileCompleted = useMemo(() => {
62
- return isProfileCompleteSuccess || (!!(profile?.first_name || profile?.last_name) && !!profile?.phone_number && !!profile?.dob)
64
+ return (
65
+ isProfileCompleteSuccess ||
66
+ (!!(profile?.first_name || profile?.last_name) && !!profile?.phone_number && !!profile?.dob)
67
+ )
63
68
  }, [profile, isProfileCompleteSuccess])
64
69
 
65
- const taskStates: Record<EarnCreditsTaskType, { finished: boolean; onClick?: () => void; link?: string }> = useMemo(() => ({
66
- [EarnCreditsTaskType.Activate]: {
67
- finished: !!profile?.activated || isActivateSuccess,
68
- onClick: isLogin
69
- ? () => pushView({
70
- type: MemberPopupPanelType.Activate,
71
- props: { onSuccess: onActivateSuccess },
72
- })
73
- : openSignUpPopup,
74
- },
75
- [EarnCreditsTaskType.CompleteProfile]: {
76
- finished: profileCompleted,
77
- onClick: isLogin
78
- ? () => pushView({
79
- type: MemberPopupPanelType.CompleteProfile,
80
- props: { onSuccess: onProfileCompleteSuccess },
81
- })
82
- : openSignUpPopup,
83
- },
84
- [EarnCreditsTaskType.Subscribe]: {
85
- finished: isSubscribed || isSubscribeSuccess,
86
- onClick: isLogin
87
- ? () => pushView({
88
- type: MemberPopupPanelType.SubscribeEmail,
89
- props: { onSuccess: onSubscribeSuccess },
90
- })
91
- : openSignUpPopup,
92
- },
93
- [EarnCreditsTaskType.Shop]: {
94
- finished: false,
95
- link: copy.goShopLink,
96
- onClick: isLogin ? undefined : openSignUpPopup,
97
- },
98
- }), [profile, isLogin, isActivateSuccess, isProfileCompleteSuccess, profileCompleted, isSubscribed, isSubscribeSuccess, openSignUpPopup, pushView, copy.goShopLink])
70
+ const taskStates: Record<EarnCreditsTaskType, { finished: boolean; onClick?: () => void; link?: string }> = useMemo(
71
+ () => ({
72
+ [EarnCreditsTaskType.Activate]: {
73
+ finished: !!profile?.activated || isActivateSuccess,
74
+ onClick: isLogin
75
+ ? () =>
76
+ pushView({
77
+ type: MemberPopupPanelType.Activate,
78
+ props: { onSuccess: onActivateSuccess },
79
+ })
80
+ : openSignUpPopup,
81
+ },
82
+ [EarnCreditsTaskType.CompleteProfile]: {
83
+ finished: profileCompleted,
84
+ onClick: isLogin
85
+ ? () =>
86
+ pushView({
87
+ type: MemberPopupPanelType.CompleteProfile,
88
+ props: { onSuccess: onProfileCompleteSuccess },
89
+ })
90
+ : openSignUpPopup,
91
+ },
92
+ [EarnCreditsTaskType.Subscribe]: {
93
+ finished: isSubscribed || isSubscribeSuccess,
94
+ onClick: isLogin
95
+ ? () =>
96
+ pushView({
97
+ type: MemberPopupPanelType.SubscribeEmail,
98
+ props: { onSuccess: onSubscribeSuccess },
99
+ })
100
+ : openSignUpPopup,
101
+ },
102
+ [EarnCreditsTaskType.Shop]: {
103
+ finished: false,
104
+ link: copy.goShopLink,
105
+ onClick: isLogin ? undefined : openSignUpPopup,
106
+ },
107
+ }),
108
+ [
109
+ profile,
110
+ isLogin,
111
+ isActivateSuccess,
112
+ isProfileCompleteSuccess,
113
+ profileCompleted,
114
+ isSubscribed,
115
+ isSubscribeSuccess,
116
+ openSignUpPopup,
117
+ pushView,
118
+ copy.goShopLink,
119
+ ]
120
+ )
99
121
 
100
122
  const list = useMemo(() => {
101
- return copy.list.map(item => {
102
- const state = taskStates[item.id]
103
- if (!state) return null
104
- const buttonLabel = state.finished
105
- ? copy.completed
106
- : isLogin
107
- ? getNotFinishedLabel(item.id, copy)
108
- : copy.joinNow
109
- return { ...item, ...state, buttonLabel }
110
- }).filter(Boolean) as (typeof copy.list[number] & {
123
+ return copy.list
124
+ .map(item => {
125
+ const state = taskStates[item.id]
126
+ if (!state) return null
127
+ const buttonLabel = state.finished
128
+ ? copy.completed
129
+ : isLogin
130
+ ? getNotFinishedLabel(item.id, copy)
131
+ : copy.joinNow
132
+ return { ...item, ...state, buttonLabel }
133
+ })
134
+ .filter(Boolean) as ((typeof copy.list)[number] & {
111
135
  finished: boolean
112
136
  onClick?: () => void
113
137
  link?: string
@@ -122,15 +146,18 @@ function EarnCredits({
122
146
  {copy.title && <h2 className="text-[24px] font-[700] leading-[1.2] text-[#080a0f]">{copy.title}</h2>}
123
147
  </div>
124
148
 
125
- {isLogin && creditInfo && (
126
- <div className={cn('flex items-center gap-[12px] bg-[#f5f6f7] p-[16px]', rounded ? 'rounded-[8px]' : 'rounded-none')}>
149
+ {isLogin && (
150
+ <div
151
+ className={cn(
152
+ 'flex items-center gap-[12px] bg-[#f5f6f7] p-[16px]',
153
+ rounded ? 'rounded-[8px]' : 'rounded-none'
154
+ )}
155
+ >
127
156
  <div className="flex flex-1 flex-col gap-[4px]">
128
- <div className="text-[14px] font-[700] text-[#1e2024]">
129
- {copy.balanceLabel ?? 'AnkerCredits Balance'}
130
- </div>
157
+ <div className="text-[14px] font-[700] text-[#1e2024]">{copy.balanceLabel ?? 'AnkerCredits Balance'}</div>
131
158
  <div className="flex items-end gap-[4px]">
132
159
  <span className="text-[24px] font-[700] leading-[1.2] text-[#1e2024]">
133
- {numberFormat(creditInfo.available_credit)}
160
+ {numberFormat(creditInfo?.available_credit ?? 0)}
134
161
  </span>
135
162
  <span className="text-[14px] font-[700] leading-[1.4] text-[#1e2024]">
136
163
  {copy.creditUnit ?? 'ankercredits'}
@@ -139,12 +166,10 @@ function EarnCredits({
139
166
  </div>
140
167
  <div className="h-full w-px bg-[#dadce0]" />
141
168
  <div className="flex flex-1 flex-col gap-[4px]">
142
- <div className="text-[14px] font-[700] text-[#1e2024]">
143
- {copy.comingSoonLabel ?? 'Coming Soon'}
144
- </div>
169
+ <div className="text-[14px] font-[700] text-[#1e2024]">{copy.comingSoonLabel ?? 'Coming Soon'}</div>
145
170
  <div className="flex items-end gap-[4px]">
146
171
  <span className="text-[24px] font-[700] leading-[1.2] text-[#1e2024]">
147
- {numberFormat(creditInfo.pending_credit ?? 0)}
172
+ {numberFormat(creditInfo?.pending_credit ?? 0)}
148
173
  </span>
149
174
  <span className="text-[14px] font-[700] leading-[1.4] text-[#1e2024]">
150
175
  {copy.creditUnit ?? 'ankercredits'}
@@ -156,9 +181,7 @@ function EarnCredits({
156
181
  </div>
157
182
 
158
183
  <div className="flex flex-col gap-[16px]">
159
- {copy.subtitle && (
160
- <p className="text-[16px] font-[700] text-[#1e2024]">{copy.subtitle}</p>
161
- )}
184
+ {copy.subtitle && <p className="text-[16px] font-[700] text-[#1e2024]">{copy.subtitle}</p>}
162
185
 
163
186
  <div className="flex flex-col">
164
187
  {list.map((item, idx) => (
@@ -177,7 +200,10 @@ function EarnCredits({
177
200
  href={item.link}
178
201
  target="_blank"
179
202
  rel="noopener noreferrer"
180
- className={cn('cursor-pointer bg-[#080a0f] min-w-[118px] px-[20px] pb-[10px] pt-[11px] text-[14px] font-[700] leading-[1.2] text-white', rounded ? 'rounded-[4px]' : 'rounded-none')}
203
+ className={cn(
204
+ 'cursor-pointer bg-[#080a0f] min-w-[118px] px-[20px] pb-[10px] pt-[11px] text-[14px] font-[700] leading-[1.2] text-white',
205
+ rounded ? 'rounded-[4px]' : 'rounded-none'
206
+ )}
181
207
  >
182
208
  {item.buttonLabel}
183
209
  </a>
@@ -205,10 +231,14 @@ function EarnCredits({
205
231
 
206
232
  function getNotFinishedLabel(id: EarnCreditsTaskType, copy: EarnCreditsCopy): string {
207
233
  switch (id) {
208
- case EarnCreditsTaskType.Activate: return copy.activate
209
- case EarnCreditsTaskType.CompleteProfile: return copy.completeProfileLabel
210
- case EarnCreditsTaskType.Subscribe: return copy.subscribeLabel
211
- case EarnCreditsTaskType.Shop: return copy.shopNow
234
+ case EarnCreditsTaskType.Activate:
235
+ return copy.activate
236
+ case EarnCreditsTaskType.CompleteProfile:
237
+ return copy.completeProfileLabel
238
+ case EarnCreditsTaskType.Subscribe:
239
+ return copy.subscribeLabel
240
+ case EarnCreditsTaskType.Shop:
241
+ return copy.shopNow
212
242
  }
213
243
  }
214
244