{
return input
})
-const linkQueryInput = ref('')
-const linkQueryDefault = computed(() => {
- const isUseLikerLandLink = linkSetting.value === 'liker_land'
- let utmSource = isUseLikerLandLink ? 'likerland' : 'stripe'
- if (isCustomLink.value) {
- utmSource = 'custom-link'
+function constructUTMQueryString (input: {
+ utmCampaign?: string,
+ utmSource?: string,
+ utmMedium?: string
+} = {}) {
+ const searchParams = new URLSearchParams()
+ if (input.utmCampaign) {
+ searchParams.set('utm_campaign', input.utmCampaign)
+ }
+ if (input.utmSource) {
+ searchParams.set('utm_source', input.utmSource)
+ }
+ if (input.utmMedium) {
+ searchParams.set('utm_medium', input.utmMedium)
}
+ return searchParams.toString().replace('?', '')
+}
+
+const utmCampaignInput = ref('')
+const utmCampaignDefault = 'bookpress'
+const shouldPrefixChannelIdForUTMCampaign = ref(true)
+
+const utmMediumInput = ref('')
+const utmMediumDefault = 'affiliate'
+
+const utmSourceInput = ref('')
+const utmSourceDefault = computed(() => {
+ const isUseLikerLandLink = destinationSetting.value === 'liker_land'
+ if (isUsingCustomDestination.value) {
+ return 'custom-link'
+ }
+ return isUseLikerLandLink ? 'likerland' : 'stripe'
+})
+
+const linkQueryInputModel = ref('')
+const additionalQueryStringInput = computed({
+ get: () => {
+ if (linkQueryInputModel.value) {
+ return linkQueryInputModel.value
+ }
+ return constructUTMQueryString({
+ utmCampaign: utmCampaignInput.value,
+ utmSource: utmSourceInput.value,
+ utmMedium: utmMediumInput.value
+ })
+ },
+ set: (value) => {
+ linkQueryInputModel.value = value
+ }
+})
+
+const additionalQueryStringInputPlaceholder = computed(() => {
+ return constructUTMQueryString({
+ utmCampaign: utmCampaignDefault,
+ utmSource: utmSourceDefault.value,
+ utmMedium: utmMediumDefault
+ })
+})
+
+const linkQueryDefault = computed(() => {
return {
- utm_medium: 'affiliate',
- utm_source: utmSource
+ utm_medium: utmMediumDefault,
+ utm_source: utmSourceDefault.value,
+ utm_campaign: utmCampaignDefault
}
})
-const linkQuery = computed(() => {
- const mergedQuery = { ...linkQueryDefault.value }
+const mergedQueryStringObject = computed
>(() => {
+ const mergedObject = { ...linkQueryDefault.value }
const input = productIdInput.value?.trim() || ''
if (input.startsWith('http')) {
- Object.assign(mergedQuery, Object.fromEntries(new URL(input).searchParams))
+ Object.assign(mergedObject, Object.fromEntries(new URL(input).searchParams))
}
- if (linkQueryInput.value) {
- Object.assign(mergedQuery, Object.fromEntries(new URLSearchParams(linkQueryInput.value)))
+ if (additionalQueryStringInput.value) {
+ Object.assign(mergedObject, Object.fromEntries(new URLSearchParams(additionalQueryStringInput.value)))
}
- return mergedQuery
+ return mergedObject
+})
+const commonQueryStringTableRows = computed(() => {
+ return Object.entries(mergedQueryStringObject.value)
+ .filter(([key]) => !(key === 'utm_campaign' && shouldPrefixChannelIdForUTMCampaign.value))
+ .map(([key, value]) => ({
+ key,
+ value
+ }))
})
-const linkQueryTableRows = computed(() => Object.entries(linkQuery.value).map(([key, value]) => ({
- key,
- value
-})))
const isCollection = computed(() => productId.value?.startsWith('col_'))
-const linkSettings = ref([
+const destinationSettings = ref([
{
name: 'Liker Land Product Page',
value: 'liker_land'
@@ -319,9 +427,9 @@ const linkSettings = ref([
value: 'custom'
}
])
-const linkSetting = ref(linkSettings.value[0].value)
-const isCustomLink = computed(() => linkSetting.value === 'custom')
-const customLinkInput = ref(route.query.custom_link as string || '')
+const destinationSetting = ref(destinationSettings.value[0].value)
+const isUsingCustomDestination = computed(() => destinationSetting.value === 'custom')
+const customDestinationURLInput = ref(route.query.custom_link as string || '')
const customChannelInput = ref('')
const customChannels = computed(
@@ -347,7 +455,14 @@ watch(customChannelInput, () => {
customChannelInputError.value = ''
})
-const isLoadingProductData = ref(false)
+const isCreatingAffiliationLinks = ref(false)
+const canCreateAffiliationLink = computed(() => {
+ if (isUsingCustomDestination.value) {
+ return !!customDestinationURLInput.value
+ }
+ return !!productId.value && !isCreatingAffiliationLinks.value
+})
+
const productData = ref(undefined)
const productName = computed(() => {
if (!productData.value) {
@@ -371,7 +486,7 @@ const priceIndexOptions = computed(() => {
})
const tableTitle = computed(() => `${productName.value ? `${productName.value} ` : ''}Affiliation Links`)
-const tableColumns = [
+const linkTableColumns = [
{
key: 'channelId',
label: 'Channel',
@@ -387,25 +502,23 @@ const tableColumns = [
sortable: false
}
]
-const tableRows = computed(() => {
- if (!productData.value) {
- return []
- }
+const linkTableRows = computed(() => {
const channels = [...customChannels.value, ...AFFILIATION_CHANNELS]
return channels.map((channel) => {
- let utmCampaign = 'bookpress'
- if (channel.id !== AFFILIATION_CHANNEL_DEFAULT) {
+ const utmCampaignInput = mergedQueryStringObject.value.utm_campaign
+ let utmCampaign = utmCampaignInput || utmCampaignDefault
+ if (shouldPrefixChannelIdForUTMCampaign.value && channel.id !== AFFILIATION_CHANNEL_DEFAULT) {
utmCampaign = `${convertChannelIdToLikerId(channel.id)}_${utmCampaign}`
}
- const urlConfig = {
- [isCollection.value ? 'collectionId' : 'classId']: productId.value,
+ const urlConfig: any = {
+ [isCollection.value ? 'collectionId' : 'classId']: productId.value || '',
channel: channel.id,
priceIndex: priceIndex.value,
- customLink: isCustomLink.value ? customLinkInput.value : undefined,
- isUseLikerLandLink: linkSetting.value === 'liker_land',
+ customLink: isUsingCustomDestination.value ? customDestinationURLInput.value : undefined,
+ isUseLikerLandLink: destinationSetting.value === 'liker_land',
query: {
utm_campaign: utmCampaign,
- ...linkQuery.value
+ ...mergedQueryStringObject.value
}
}
return {
@@ -415,7 +528,7 @@ const tableRows = computed(() => {
url: getPurchaseLink(urlConfig),
qrCodeUrl: getPurchaseLink({
...urlConfig,
- isForQRCode: linkQuery.value.utm_source === linkQueryDefault.value.utm_source
+ isForQRCode: mergedQueryStringObject.value.utm_source === linkQueryDefault.value.utm_source
})
}
})
@@ -458,51 +571,76 @@ async function createAffiliationLink () {
productData.value = undefined
customChannelInputError.value = ''
- if (!productId.value) {
+ if (!canCreateAffiliationLink.value) {
return
}
- // Validate custom channels
- if (customChannels.value.length) {
- const invalidChannel = customChannels.value.find(channel => !validateChannelId(channel.id))
- if (invalidChannel) {
- customChannelInputError.value = `Invalid channel "${invalidChannel.id}", please enter a valid channel ID starting with "@"`
- return
- }
+ try {
+ isCreatingAffiliationLinks.value = true
+
+ // Validate custom channels
+ if (customChannels.value.length) {
+ const invalidChannel = customChannels.value.find(channel => !validateChannelId(channel.id))
+ if (invalidChannel) {
+ customChannelInputError.value = `Invalid channel "${invalidChannel.id}", please enter a valid channel ID starting with "@"`
+ return
+ }
- try {
- await Promise.all(customChannels.value.map(async (channel) => {
- const channelInfo = await likerStore.lazyFetchChannelInfoById(channel.id)
- if (!channelInfo) {
- throw new Error(`Channel ID "${channel.id}" has not registered for Liker ID`)
- }
-
- const stripeConnectStatus = await stripeStore.fetchStripeConnectStatusByWallet(channelInfo.likeWallet)
- if (!stripeConnectStatus?.hasAccount) {
- throw new Error(`Channel ID "${channel.id}" has not connected to Stripe`)
- }
- if (!stripeConnectStatus?.isReady) {
- throw new Error(`Channel ID "${channel.id}" has not completed Stripe Connect setup`)
- }
- }))
- } catch (error) {
- customChannelInputError.value = (error as Error).message
- return
+ try {
+ await Promise.all(customChannels.value.map(async (channel) => {
+ const channelInfo = await likerStore.lazyFetchChannelInfoById(channel.id)
+ if (!channelInfo) {
+ throw new Error(`Channel ID "${channel.id}" has not registered for Liker ID`)
+ }
+
+ const stripeConnectStatus = await stripeStore.fetchStripeConnectStatusByWallet(channelInfo.likeWallet)
+ if (!stripeConnectStatus?.hasAccount) {
+ throw new Error(`Channel ID "${channel.id}" has not connected to Stripe`)
+ }
+ if (!stripeConnectStatus?.isReady) {
+ throw new Error(`Channel ID "${channel.id}" has not completed Stripe Connect setup`)
+ }
+ }))
+ } catch (error) {
+ customChannelInputError.value = (error as Error).message
+ return
+ }
}
- }
- isLoadingProductData.value = true
- const data = await fetchProductData()
- isLoadingProductData.value = false
- if (data) {
- productData.value = data
+ if (productId.value) {
+ const data = await fetchProductData()
+ if (data) {
+ productData.value = data
+ }
+ } else {
+ productData.value = null
+ }
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error)
+ toast.add({
+ icon: 'i-heroicons-exclamation-circle',
+ title: 'Failed to create affiliation link',
+ timeout: 0,
+ color: 'red',
+ ui: {
+ title: 'text-red-400 dark:text-red-400'
+ }
+ })
+ } finally {
+ isCreatingAffiliationLinks.value = false
}
}
function getQRCodeFilename (channel = '') {
- const filenameParts = [`${productName.value || productId.value}`]
- if (!isCollection.value) {
+ const filenameParts: string[] = []
+ if (isUsingCustomDestination.value) {
+ const url = new URL(customDestinationURLInput.value)
+ filenameParts.push(url.hostname)
+ } else if (isCollection.value) {
filenameParts.push(`price_${priceIndex.value}`)
+ } else {
+ filenameParts.push(`${productName.value || productId.value}`)
}
if (channel) {
filenameParts.push(`channel_${channel}`)
@@ -522,7 +660,7 @@ async function copyLink (text = '') {
function downloadAllPurchaseLinks () {
downloadFile({
- data: tableRows.value,
+ data: linkTableRows.value,
fileName: `${productName.value}_purchase_links.csv`,
fileType: 'csv'
})
@@ -532,7 +670,7 @@ function printAllQRCodes () {
try {
sessionStorage.setItem(
'nft_book_press_batch_qrcode',
- convertArrayOfObjectsToCSV(tableRows.value.map(({ channelId, qrCodeUrl, ...link }) => ({ key: channelId, ...link, url: qrCodeUrl })))
+ convertArrayOfObjectsToCSV(linkTableRows.value.map(({ channelId, qrCodeUrl, ...link }) => ({ key: channelId, ...link, url: qrCodeUrl })))
)
window.open('/batch-qrcode?print=1', 'batch_qrcode', 'popup,menubar=no,location=no,status=no')
} catch (error) {
@@ -554,7 +692,7 @@ function shortenAllLinks () {
try {
sessionStorage.setItem(
'nft_book_press_batch_shorten_url',
- convertArrayOfObjectsToCSV(tableRows.value.map(({ channelId, ...link }) => ({ key: channelId, ...link })))
+ convertArrayOfObjectsToCSV(linkTableRows.value.map(({ channelId, ...link }) => ({ key: channelId, ...link })))
)
router.push({ name: 'batch-short-links', query: { print: 1 } })
} catch (error) {
@@ -573,7 +711,7 @@ function shortenAllLinks () {
}
async function downloadAllQRCodes () {
- const items = tableRows.value.map(link => ({
+ const items = linkTableRows.value.map(link => ({
url: link.qrCodeUrl,
filename: getQRCodeFilename(link.channelId)
}))
diff --git a/utils/index.ts b/utils/index.ts
index ce6934508..d409afa9d 100644
--- a/utils/index.ts
+++ b/utils/index.ts
@@ -135,19 +135,15 @@ export function getPurchaseLink ({
if (classId) {
query.price_index = priceIndex.toString()
}
- if (customLink) {
- const url = new URL(customLink)
- Object.entries(query).forEach(([key, value]) => {
- url.searchParams.set(key, value)
- })
- return url.toString()
- }
if (isForQRCode) {
query.utm_medium = queryInput?.utm_medium ? `${queryInput.utm_medium}-qr` : 'qrcode'
}
const { LIKE_CO_API, LIKER_LAND_URL } = useRuntimeConfig().public
const queryString = `?${new URLSearchParams({ ...queryInput, ...query }).toString()}`
+ if (customLink) {
+ return `${customLink}${queryString}`
+ }
if (collectionId) {
return isUseLikerLandLink
? `${LIKER_LAND_URL}/nft/collection/${collectionId}${queryString}`