const adPlugin = requirePlugin("uni-ad"); import { canShowHalfScreenModal, isHalfScreenModalShowing, markHalfScreenModalConfirmed, markHalfScreenModalDismissed, markHalfScreenModalShowing, queueHalfScreenModalOpenOptions, resetHalfScreenModalSession, resolveHalfScreenModalContent, takePendingHalfScreenModalOpenOptions, unlockHalfScreenModalShowing } from '../../lib-x/ad/half-screen-modal.js' const EventType = { Load: 'load', Close: 'close', Error: 'error' } const AdType = { Banner: "banner", RewardedVideo: "rewardedVideo", Interstitial: "interstitial" } const ProviderType = { WeChat: 10018, UserWeChat: 10017, ShanHu: 10020, HalfScreen: 10032 } /** 微信广告无填充错误码 */ const WxAdErrorCode = { NoFill: 1004 } /** 供 uniad 壳组件显式声明,消除 Vue3 编译期 mixin 不可见告警 */ export const adComponentProps = { options: { type: [Object, Array], default () { return {} } }, adpid: { type: [Number, String], default: '' }, unitId: { type: [Number, String], default: '' }, preload: { type: [Boolean, String], default: true }, loadnext: { type: [Boolean, String], default: false }, urlCallback: { type: Object, default () { return {} } } } /** 供 uniad 壳组件显式声明的 data */ export function adComponentData () { return { loading: false, userwx: false, userUnitId: '', wxchannel: false, errorMessage: null, isHalfScreen: false } } /** 供 uniad 壳组件显式声明的 emits */ export const adComponentEmits = ['load', 'close', 'error', 'adcreated'] export default { emits: adComponentEmits, props: adComponentProps, data: adComponentData, created () { resetHalfScreenModalSession() this._ad = null this._loading = false this._wxRewardedAd = null this._wxInterstitialAd = null this._providerType = ProviderType.ShanHu this._halfScreenOpening = false this._halfScreenOpeningTimer = null if (this.preload && this._canCreateAd()) { this.load() } }, methods: { load () { if (this.loading) { return } this._startLoading() if (this._providerType == ProviderType.ShanHu) { } else if (this._providerType == ProviderType.WeChat) { this.selectComponent('.uniad-plugin-wx').load() } else if (this._providerType == ProviderType.UserWeChat) { this._loadWxAd() } }, show (e) { this.errorMessage = null const plugin = this.selectComponent('.uniad-plugin') if (this._isHalfScreenAd(plugin)) { return } if (this._providerType == ProviderType.ShanHu) { this._showAdInPlugin(this.selectComponent('.uniad-plugin')) } else if (this._providerType == ProviderType.WeChat) { this._showAdInPlugin(this.selectComponent('.uniad-plugin-wx')) } else if (this._providerType == ProviderType.UserWeChat) { this._showWxAd(e) } }, _onclick () { if (this.isHalfScreen) { return } this.show() }, _startLoading () { this.loading = true this.errorMessage = null }, _canCreateAd () { let result = false if (typeof this.adpid === 'string' && this.adpid.length > 0) { result = true } else if (typeof this.adpid === 'number') { result = true } return result }, _hasCallback () { return (typeof this.urlCallback === 'object' && Object.keys(this.urlCallback).length > 0) }, /** * 按 provider 从 acsv4 配置列表查找广告项 * @param {Array} list 配置数组 * @param {number} provider 渠道 provider */ _findAdItemByProvider (list, provider) { if (!Array.isArray(list)) { return null } return list.find(item => Number(item.provider) === Number(provider)) || null }, /** * 是否为微信广告无填充 errCode 1004 * @param {Object} err 错误对象 */ _isWxAdNoFillError (err) { if (!err) { return false } const detail = err.detail || err return Number(detail.errCode) === WxAdErrorCode.NoFill }, /** * 代运营无填充时尝试半屏兜底 * @param {Object} plugin uniad-plugin 实例 * @param {Object} err 错误对象 */ _tryHalfScreenFallback (plugin, err) { if (!plugin || !plugin.tryHalfScreenFallback) { return false } const detail = err && (err.detail || err) if (!this._isWxAdNoFillError(detail)) { return false } return plugin.tryHalfScreenFallback(detail) }, /** * 处理代运营微信广告错误,无填充时走半屏兜底 * @param {Object} err 错误对象 */ _handleUserWxAdError (err) { this.loading = false const plugin = this.selectComponent('.uniad-plugin') if (this._tryHalfScreenFallback(plugin, err)) { return true } this.errorMessage = JSON.stringify(err) this._dispatchEvent(EventType.Error, err) return false }, /** * 切换为半屏广告模式 */ _setHalfScreenMode () { this.isHalfScreen = true this._providerType = ProviderType.HalfScreen this.userwx = false this.wxchannel = false this.loading = false }, /** * 是否半屏广告 * @param {Object} plugin uniad-plugin 实例 */ _isHalfScreenAd (plugin) { return this._providerType === ProviderType.HalfScreen || !!( plugin && plugin.getHalfScreenConfig && plugin.getHalfScreenConfig() ) }, /** * 用户点击半屏占位(插件 halfScreenTap 事件) * @param {Object} e 事件对象 */ _onHalfScreenTap (e) { const openOptions = e.detail && e.detail.openOptions if (!openOptions) { return } this._openTargetMiniProgramInHost(openOptions) }, /** * 开始打开小程序防抖锁 */ _beginMiniProgramOpening () { this._halfScreenOpening = true this._halfScreenOpeningTimer = setTimeout(() => { this._resetHalfScreenOpening() }, 3000) }, /** * 用户手动触发:优先半屏打开,失败则 navigateToMiniProgram 兜底 * @param {Object} openOptions 打开参数 */ _openTargetMiniProgramInHost (openOptions) { if (this._halfScreenOpening) { return } if (!openOptions || !openOptions.appId) { this._dispatchEvent(EventType.Error, { errMsg: 'mini program openOptions unavailable' }) return } const plugin = this.selectComponent('.uniad-plugin') this._openHalfScreenEmbeddedInHost(openOptions, plugin) }, /** * 半屏嵌入打开 wx.openEmbeddedMiniProgram * @param {Object} openOptions 打开参数 * @param {Object} plugin uniad-plugin 实例 */ _openHalfScreenEmbeddedInHost (openOptions, plugin) { if (typeof wx.openEmbeddedMiniProgram !== 'function') { this._openMiniProgramDirectInHost(openOptions, plugin) return } this._beginMiniProgramOpening() wx.openEmbeddedMiniProgram({ appId: openOptions.appId, path: openOptions.path, extraData: openOptions.extraData || {}, success: (res) => { this._resetHalfScreenOpening() if (plugin && plugin.notifyHalfScreenOpenSuccess) { plugin.notifyHalfScreenOpenSuccess(res) } }, fail: (err) => { if (this._isMiniProgramOpenCancel(err)) { this._resetHalfScreenOpening() this._notifyHalfScreenOpenResult(plugin, err) return } this._openMiniProgramDirectInHost(openOptions, plugin, err) } }) }, /** * 直接打开小程序 wx.navigateToMiniProgram(半屏未过审等场景兜底) * @param {Object} openOptions 打开参数 * @param {Object} plugin uniad-plugin 实例 * @param {Object} fromErr 半屏打开失败时的错误 */ _openMiniProgramDirectInHost (openOptions, plugin, fromErr) { if (typeof wx.navigateToMiniProgram !== 'function') { this._resetHalfScreenOpening() this._notifyHalfScreenOpenResult(plugin, fromErr || { errMsg: 'navigateToMiniProgram not supported' }) return } if (!this._halfScreenOpening) { this._beginMiniProgramOpening() } wx.navigateToMiniProgram({ appId: openOptions.appId, path: openOptions.path, extraData: openOptions.extraData || {}, success: (res) => { this._resetHalfScreenOpening() if (plugin && plugin.notifyHalfScreenOpenSuccess) { plugin.notifyHalfScreenOpenSuccess(res, { isDirect: true }) } }, fail: (err) => { this._resetHalfScreenOpening() this._notifyHalfScreenOpenResult(plugin, err) } }) }, /** * 清除半屏打开锁定时器 */ _clearHalfScreenOpeningTimer () { if (this._halfScreenOpeningTimer) { clearTimeout(this._halfScreenOpeningTimer) this._halfScreenOpeningTimer = null } }, /** * 重置半屏打开锁 */ _resetHalfScreenOpening () { this._halfScreenOpening = false this._clearHalfScreenOpeningTimer() }, /** * 是否为打开/跳转小程序时用户点击取消(fail cancel) * @param {Object} err 失败回调对象 */ _isMiniProgramOpenCancel (err) { const msg = (err && err.errMsg) || '' return typeof msg === 'string' && msg.indexOf('fail cancel') !== -1 }, /** * 通知插件半屏打开失败或用户取消 * @param {Object} plugin uniad-plugin 实例 * @param {Object} err 失败回调对象 */ _notifyHalfScreenOpenResult (plugin, err) { if (this._isMiniProgramOpenCancel(err)) { if (plugin && plugin.notifyHalfScreenOpenCancel) { plugin.notifyHalfScreenOpenCancel(err) } else if (plugin && plugin.notifyHalfScreenOpenFail) { plugin.notifyHalfScreenOpenFail(err) } else { this._dispatchEvent(EventType.Close, { type: 'userCancel', detail: err }) } return } if (plugin && plugin.notifyHalfScreenOpenFail) { plugin.notifyHalfScreenOpenFail(err) } else { this._dispatchEvent(EventType.Error, err) } }, _onHalfScreenReady () { this._setHalfScreenMode() }, /** * 插件 all_show 双广告:ad 配置就绪后并行弹窗 * @param {Object} e 事件对象 */ _onHalfScreenModal (e) { const detail = e.detail || e const openOptions = detail.openOptions if (openOptions) { this._tryShowHalfScreenModal(openOptions) } }, /** * 尝试展示排队中的半屏弹窗 */ _processPendingHalfScreenModal () { const pending = takePendingHalfScreenModalOpenOptions() if (pending) { setTimeout(() => { this._tryShowHalfScreenModal(pending) }, 50) } }, /** * 宿主层统一调用 uni.showModal 引导打开半屏 * @param {Object} openOptions 打开半屏/直达小程序参数 * @param {number} retryCount showModal 失败重试次数 */ _tryShowHalfScreenModal (openOptions, retryCount = 0) { if (!openOptions || !openOptions.appId) { return } const appId = openOptions.appId if (!canShowHalfScreenModal(appId)) { return } if (isHalfScreenModalShowing()) { queueHalfScreenModalOpenOptions(openOptions) return } markHalfScreenModalShowing(appId) /* eslint-disable no-undef */ uni.showModal({ title: '提示', content: resolveHalfScreenModalContent(openOptions.msg), confirmText: '继续', cancelText: '取消', success: (res) => { if (res.confirm) { markHalfScreenModalConfirmed() this._openTargetMiniProgramInHost(openOptions) } else { markHalfScreenModalDismissed(appId) } }, fail: () => { unlockHalfScreenModalShowing(appId) if (retryCount < 1) { setTimeout(() => { this._tryShowHalfScreenModal(openOptions, retryCount + 1) }, 300) } else { this._processPendingHalfScreenModal() } } }) }, _onmpload (e) { this.loading = false const plugin = this.selectComponent('.uniad-plugin') if (plugin && plugin.getHalfScreenConfig && plugin.getHalfScreenConfig()) { this._setHalfScreenMode() } this._dispatchEvent(EventType.Load, {}) }, _onmpclose (e) { this._dispatchEvent(EventType.Close, e.detail) if (e.detail.adsdata) { const adv = e.detail.adv const adsdata = e.detail.adsdata const version = e.detail.version /* eslint-disable no-undef */ uniCloud.callFunction({ name: 'uniAdCallback', data: { adv: adv, adsdata: adsdata, version: version }, secretType: 'both', success: (res) => { }, fail: (err) => { this._dispatchEvent(EventType.Error, err) } }) delete e.detail.adv delete e.detail.adsdata delete e.detail.version } }, _onmperror (e) { const detail = e.detail || e if (this._handleUserWxAdError(detail)) { return } if (this._isMiniProgramOpenCancel(detail)) { return } this._dispatchEvent(EventType.Error, detail) }, _onnextchannel (e) { const adData = this._findAdItemByProvider(e.detail, ProviderType.UserWeChat) if (!adData) { return } this.wxchannel = true this.$nextTick(() => { if (adData.provider == 10017) { this._providerType = ProviderType.UserWeChat switch(adData._nt_) { case 4: this.wxAdType = AdType.Banner this.userwx = true this.userUnitId = adData.posid break; case 9: this.wxAdType = AdType.RewardedVideo this._createRewardedAd(adData.posid) break; case 15: this.wxAdType = AdType.Interstitial this._createInterstitialAd(adData.posid) break; } } else if (adData.provider == 10018) { this._providerType = ProviderType.WeChat this.selectComponent('.uniad-plugin-wx').setConfig(adData) } }) }, _onwxchannelerror(e) { this.wxchannel = false this.$nextTick(() => { this._providerType = ProviderType.ShanHu this.selectComponent('.uniad-plugin').shanhuChannel() }) }, _dispatchEvent (type, data) { this.$emit(type, { detail: data }) }, _showAdInPlugin(adComponent) { if (this._hasCallback()) { const userCryptoManager = wx.getUserCryptoManager() userCryptoManager.getLatestUserKey({ success: ({ encryptKey, iv, version, expireTime }) => { adComponent.show({ userId: this.urlCallback.userId || '', extra: this.urlCallback.extra || '', encryptKey, iv, version, expireTime }) }, fail: (err) => { this._dispatchEvent(EventType.Error, err) } }) } else { adComponent.show({ userId: this.urlCallback.userId || '', extra: this.urlCallback.extra || '' }) } }, _loadWxAd () { switch (this.wxAdType) { case AdType.RewardedVideo: if (this._wxRewardedAd) { this._wxRewardedAd.load(); } break; case AdType.Interstitial: if (this._wxInterstitialAd) { this._wxInterstitialAd.load(); } break; } }, // 加载/显示广告 _showWxAd (options) { this._urlCallback = options || this.urlCallback; if (this.loading == true) { this._userInvokeShowFlag = true return } switch (this.wxAdType) { case AdType.RewardedVideo: if (!this._wxRewardedAd) { return; } this._wxRewardedAd.show().catch((err) => { if (this._handleUserWxAdError(err)) { return } this._wxRewardedAd.load().then(() => { this._wxRewardedAd.show(); }).catch((retryErr) => { this._handleUserWxAdError(retryErr) }); }); break; case AdType.Interstitial: if (!this._wxInterstitialAd) { return; } this._wxInterstitialAd.show().catch((err) => { if (this._handleUserWxAdError(err)) { return } this._wxInterstitialAd.load().then(() => { this._wxInterstitialAd.show(); }).catch((retryErr) => { this._handleUserWxAdError(retryErr) }); }); break; } }, // 微信激励视频 _createRewardedAd(adUnitId) { if (this._wxRewardedAd) { return; } this._wxRewardedAd = wx.createRewardedVideoAd({ adUnitId: adUnitId, multiton: true }); this._wxRewardedAd.onLoad(() => { this.loading = false this._dispatchEvent(EventType.Load, {}) if (this._userInvokeShowFlag) { this._userInvokeShowFlag = false; this._wxRewardedAd.show(); } }); this._wxRewardedAd.onError(err => { this._handleUserWxAdError(err) }); this._wxRewardedAd.onClose(res => { if (res.isEnded) { this._callServer() } else { this._dispatchEvent(EventType.Close, res); } }); this.loading = true }, // 微信插屏 _createInterstitialAd(adUnitId) { if (this._wxInterstitialAd) { return; } this._wxInterstitialAd = wx.createInterstitialAd({ adUnitId: adUnitId }); this._wxInterstitialAd.onLoad(() => { this.loading = false this._dispatchEvent(EventType.Load, {}) if (this._userInvokeShowFlag) { this._userInvokeShowFlag = false; this._wxInterstitialAd.show(); } }); this._wxInterstitialAd.onError(err => { this._handleUserWxAdError(err) }); this._wxInterstitialAd.onClose(res => { this._dispatchEvent(EventType.Close, res); }); this.loading = true }, _callServer() { const userCryptoManager = wx.getUserCryptoManager() userCryptoManager.getLatestUserKey({ success: (encryptConfig) => { const callbackData = adPlugin.buildCallbackData(this.adpid, this.urlCallback, {}, encryptConfig) uniCloud.callFunction({ name: 'uniAdCallback', data: callbackData, secretType: 'both', success: (res) => { this._dispatchEvent(EventType.Close, res); }, fail: (err) => { this._dispatchEvent(EventType.Error, err); } }) }, fail: (err) => { this._dispatchEvent(EventType.Error, err); } }) } } }