feat(binding): verify 4G relations before Banban unbind
This commit is contained in:
@@ -3,6 +3,7 @@ export default defineAppConfig({
|
||||
"pages/device/index",
|
||||
"pages/login/index",
|
||||
"pages/bind/index",
|
||||
"pages/traffic-binding/index",
|
||||
"pages/notification/index",
|
||||
"pages/chat/index",
|
||||
"pages/chat/detail/index",
|
||||
@@ -12,6 +13,7 @@ export default defineAppConfig({
|
||||
"pages/family-invite/index",
|
||||
"pages/webview/index",
|
||||
],
|
||||
navigateToMiniProgramAppIdList: ['wx56e1cfc52aab630f'],
|
||||
window: {
|
||||
backgroundTextStyle: "light",
|
||||
navigationBarBackgroundColor: "#FFF9EA",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import './app.scss'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { useDidShow } from '@tarojs/taro'
|
||||
import { captureTrafficReturn } from './services/traffic-binding'
|
||||
|
||||
function App(props: PropsWithChildren) {
|
||||
useDidShow((options) => { captureTrafficReturn(options) })
|
||||
return props.children
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
validateChildNameInput,
|
||||
} from '@/services/child'
|
||||
import { useSystemBanner } from '@/components/system-banner/use-system-banner'
|
||||
import { trafficPageUrl } from '@/services/traffic-binding'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
@@ -678,6 +679,10 @@ export default function Bind() {
|
||||
<Button className='bind-result-button primary' onClick={() => Taro.reLaunch({ url: bindResultDestination })}>
|
||||
{bindResultDestination.includes('/sleep/') ? '去关联宝贝' : '返回首页'}
|
||||
</Button>
|
||||
{!isAddingCard && <Button className='bind-result-button primary' onClick={() => Taro.navigateTo({ url: trafficPageUrl(boundDeviceId) })}>
|
||||
关联流量服务
|
||||
</Button>}
|
||||
{!isAddingCard && <Text className='bind-result-description'>设备已绑定。流量查询和充值需另外关联,可稍后在管理页完成。</Text>}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -10,6 +10,7 @@ import Taro, { useDidHide, useDidShow, useShareAppMessage } from '@tarojs/taro'
|
||||
import { authorizeParentPhone, clearToken, getParent, getToken, Parent } from '@/services/auth'
|
||||
import { DEVICE_UNAVAILABLE_MESSAGE } from '@/services/api'
|
||||
import { getCurrentUserId } from '@/services/session'
|
||||
import { TrafficOverview, getTrafficOverview, trafficLinked, openTrafficPage, openTrafficPortal, flushTrafficReceipt } from '@/services/traffic-binding'
|
||||
import {
|
||||
BindingListItem,
|
||||
BindingCardItem,
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
loadCurrentChildBindingContext,
|
||||
setSelectedBindingDeviceId,
|
||||
updateBindingCardName,
|
||||
unbindDevice,
|
||||
} from '@/services/binding'
|
||||
import {
|
||||
Child,
|
||||
@@ -77,6 +77,20 @@ export default function Sleep() {
|
||||
const [children, setChildren] = useState<Child[]>([])
|
||||
const [bindings, setBindings] = useState<BindingListItem[]>([])
|
||||
const [binding, setBinding] = useState<BindingListItem | null>(null)
|
||||
const [trafficOverview, setTrafficOverview] = useState<TrafficOverview | null>(null)
|
||||
const [trafficBusy, setTrafficBusy] = useState(false)
|
||||
const trafficLock = useRef(false)
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
setTrafficOverview(null)
|
||||
if (binding?.device_id) {
|
||||
void flushTrafficReceipt().catch(() => null)
|
||||
.then(() => getTrafficOverview(binding.device_id))
|
||||
.then((value) => { if (!cancelled) setTrafficOverview(value) })
|
||||
.catch(() => { /* Core device management remains available. */ })
|
||||
}
|
||||
return () => { cancelled = true }
|
||||
}, [binding])
|
||||
const [currentChild, setCurrentChild] = useState<Child | null>(null)
|
||||
const [firmwareStatus, setFirmwareStatus] = useState<DeviceFirmwareStatus | null>(null)
|
||||
const [showFirmwareProgress, setShowFirmwareProgress] = useState(false)
|
||||
@@ -651,12 +665,7 @@ export default function Sleep() {
|
||||
if (!result.confirm) return
|
||||
|
||||
try {
|
||||
await unbindDevice(targetBinding.device_id)
|
||||
if (binding?.device_id === targetBinding.device_id) {
|
||||
clearSelectedBindingDeviceId()
|
||||
}
|
||||
Taro.showToast({ title: '已删除设备', icon: 'success' })
|
||||
await loadData()
|
||||
await openTrafficPage(targetBinding.device_id, 'unbind')
|
||||
} catch (error: any) {
|
||||
Taro.showToast({
|
||||
title: error?.message || '删除设备失败,请重试',
|
||||
@@ -834,10 +843,7 @@ export default function Sleep() {
|
||||
if (!result.confirm) return
|
||||
|
||||
try {
|
||||
await unbindDevice(binding.device_id)
|
||||
clearSelectedBindingDeviceId()
|
||||
Taro.showToast({ title: '已解绑', icon: 'success' })
|
||||
await loadData()
|
||||
await openTrafficPage(binding.device_id, 'unbind')
|
||||
} catch (error: any) {
|
||||
Taro.showToast({
|
||||
title: error?.message || '解绑失败,请重试',
|
||||
@@ -956,6 +962,18 @@ export default function Sleep() {
|
||||
return
|
||||
}
|
||||
|
||||
if (['关联流量服务', '继续确认流量服务', '继续解绑', '流量查询', '套餐充值'].includes(item.name)) {
|
||||
if (!binding || trafficLock.current) return
|
||||
trafficLock.current = true
|
||||
setTrafficBusy(true)
|
||||
const operation = item.name === '流量查询' || item.name === '套餐充值'
|
||||
? openTrafficPortal(binding.device_id, item.name === '套餐充值' ? 'recharge' : 'device')
|
||||
: openTrafficPage(binding.device_id, item.name === '继续解绑' ? 'unbind' : 'bind')
|
||||
void operation.catch((error: any) => Taro.showToast({ title: error.message || error.errMsg || '操作未完成,请重试', icon: 'none' }))
|
||||
.finally(() => { trafficLock.current = false; setTrafficBusy(false) })
|
||||
return
|
||||
}
|
||||
|
||||
if (item.name === '我的卡片') {
|
||||
setShowCardsModal(true)
|
||||
return
|
||||
@@ -1129,6 +1147,14 @@ export default function Sleep() {
|
||||
value: binding?.device_id ? `当前: ${binding.device_id}` : '未绑定',
|
||||
arrow: true,
|
||||
},
|
||||
...(binding ? (trafficLinked(trafficOverview) && trafficOverview?.device_id === binding.device_id
|
||||
? [
|
||||
{ icon: require('../../assets/ui/menu-binding.png'), iconBgClass: 'green', name: '流量查询', value: '', arrow: true, disabled: !isFamilyOwner || trafficBusy },
|
||||
{ icon: require('../../assets/ui/menu-binding.png'), iconBgClass: 'green', name: '套餐充值', value: '', arrow: true, disabled: !isFamilyOwner || trafficBusy },
|
||||
]
|
||||
: [{ icon: require('../../assets/ui/menu-binding.png'), iconBgClass: 'green',
|
||||
name: trafficOverview?.unbind ? '继续解绑' : trafficOverview?.bind ? '继续确认流量服务' : '关联流量服务',
|
||||
value: isFamilyOwner ? '不影响伴伴使用' : '仅设备归属账号可操作', arrow: true, disabled: !isFamilyOwner || trafficBusy }]) : []),
|
||||
{
|
||||
icon: require('../../assets/ui/menu-cards.png'),
|
||||
iconBgClass: 'green',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export default definePageConfig({ navigationBarTitleText: '流量服务关联' })
|
||||
@@ -0,0 +1,7 @@
|
||||
.traffic-binding-page {
|
||||
min-height: 100vh; box-sizing: border-box; padding: 72rpx 40rpx;
|
||||
.title { display: block; font-size: 38rpx; font-weight: bold; margin-bottom: 30rpx; }
|
||||
.description { display: block; font-size: 28rpx; line-height: 1.7; margin-bottom: 30rpx; }
|
||||
.error { display: block; color: #a52a2a; margin-bottom: 24rpx; }
|
||||
button { margin-top: 24rpx; }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
|
||||
import { useRef, useState } from 'react'
|
||||
import { getToken } from '@/services/auth'
|
||||
import { clearSelectedBindingDeviceId } from '@/services/binding'
|
||||
import { getPageStyle } from '@/utils/page-style'
|
||||
import { TrafficOperation, getTrafficOperation, resumeTrafficOperation,
|
||||
flushTrafficReceipt, finishTrafficUnbind, jumpTraffic } from '@/services/traffic-binding'
|
||||
import './index.scss'
|
||||
|
||||
export default function TrafficBindingPage() {
|
||||
const { params } = useRouter()
|
||||
const deviceId = String(params.device_id || '')
|
||||
const action = params.action === 'unbind' ? 'unbind' : 'bind'
|
||||
const [operation, setOperation] = useState<TrafficOperation | null>(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const lock = useRef(false)
|
||||
|
||||
const recover = async () => {
|
||||
if (lock.current) return
|
||||
if (!getToken()) { Taro.navigateTo({ url: '/pages/login/index' }); return }
|
||||
lock.current = true
|
||||
setBusy(true)
|
||||
try {
|
||||
const current = await resumeTrafficOperation(deviceId, action)
|
||||
setOperation(current)
|
||||
if (action === 'unbind' && current.status === 'completed') clearSelectedBindingDeviceId()
|
||||
setError('')
|
||||
} catch (e: any) { setError(e.message || '暂时无法确认,请重试') }
|
||||
finally { lock.current = false; setBusy(false) }
|
||||
}
|
||||
useDidShow(() => { void recover() })
|
||||
|
||||
const proceed = async () => {
|
||||
if (!operation) { await recover(); return }
|
||||
if (lock.current) return
|
||||
lock.current = true
|
||||
setBusy(true)
|
||||
setError('')
|
||||
try {
|
||||
await flushTrafficReceipt()
|
||||
let current = await getTrafficOperation(operation.operation_id)
|
||||
if (current.status !== 'completed') {
|
||||
const result = await jumpTraffic(current)
|
||||
if (!result) return
|
||||
current = result
|
||||
if (action === 'unbind') current = await finishTrafficUnbind(current)
|
||||
}
|
||||
setOperation(current)
|
||||
if (action === 'unbind' && current.status === 'completed') clearSelectedBindingDeviceId()
|
||||
} catch (e: any) { setError(e.message || e.errMsg || '操作未完成,请继续重试') }
|
||||
finally { lock.current = false; setBusy(false) }
|
||||
}
|
||||
const completed = operation?.status === 'completed'
|
||||
const finalUnbind = action === 'unbind' && operation?.traffic_status === 'server_confirmed'
|
||||
return <View className='traffic-binding-page' style={getPageStyle(true)}>
|
||||
<Text className='title'>{action === 'bind' ? completed ? '流量服务已关联' : '设备已绑定,流量服务待关联' : completed ? '设备已解除绑定' : '解除设备与流量服务绑定'}</Text>
|
||||
<Text className='description'>{action === 'bind'
|
||||
? completed ? '可以查询流量和购买套餐了。' : '伴伴设备可以正常使用。继续前往流量管理小程序登录并关联,也可以稍后完成。'
|
||||
: completed ? '设备已解绑,流量侧已确认没有本次有效绑定。' : finalUnbind ? '流量后端已确认可以解绑,点击完成伴伴解绑。' : '先确认流量侧原绑定已解除,再解除伴伴绑定;套餐和充值记录保留。继续处理会停止本次流量关联,暂不支持取消恢复。'}</Text>
|
||||
{operation?.last_error === 'HANDOFF_RELATION_UNKNOWN' && <Text className='error'>流量后端无法确认原绑定记录,伴伴绑定仍保留,请联系服务人员核查。</Text>}
|
||||
{operation?.last_error === 'TRAFFIC_UNBIND_REQUIRED' && <Text className='description'>原流量绑定仍有效,请继续前往流量管理小程序解绑。</Text>}
|
||||
{operation?.status === 'partial_failed' && <Text className='description'>上次流量操作未完成,可以继续重试。</Text>}
|
||||
{error && <Text className='error'>{error}</Text>}
|
||||
{!completed && <Button disabled={busy} onClick={proceed}>{busy ? '处理中…' : finalUnbind ? '完成伴伴解绑' : '继续处理'}</Button>}
|
||||
<Button disabled={busy} onClick={() => Taro.switchTab({ url: '/pages/sleep/index' })}>{completed ? '返回管理' : '稍后处理'}</Button>
|
||||
</View>
|
||||
}
|
||||
@@ -12,7 +12,7 @@ const ERROR_DETAIL_MAP: Record<string, string> = {
|
||||
}
|
||||
|
||||
class ApiError extends Error {
|
||||
constructor(public status: number, message: string) {
|
||||
constructor(public status: number, message: string, public code?: string) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ function getErrorDetail(data: any): string {
|
||||
if (!data) return ERROR_DETAIL_MAP['Request failed']
|
||||
if (typeof data.errMsg === 'string') return data.errMsg
|
||||
if (typeof data.detail === 'string') return ERROR_DETAIL_MAP[data.detail] || data.detail
|
||||
if (typeof data.detail?.message === 'string') return data.detail.message
|
||||
if (Array.isArray(data.detail)) {
|
||||
return data.detail.map((item: any) => String(item?.msg || item)).join('; ')
|
||||
}
|
||||
@@ -48,25 +49,33 @@ async function request<T>(
|
||||
}
|
||||
|
||||
let response
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
response = await Taro.request({
|
||||
const task = Taro.request({
|
||||
url: requestUrl,
|
||||
method: requestMethod,
|
||||
data: options.data,
|
||||
header: headers,
|
||||
timeout: REQUEST_TIMEOUT_MS,
|
||||
})
|
||||
response = await Promise.race([task, new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
reject(new Error('请求超时,请重试'))
|
||||
task.abort?.()
|
||||
}, REQUEST_TIMEOUT_MS)
|
||||
})])
|
||||
} catch (error: any) {
|
||||
const message = String(error?.errMsg || error?.message || 'request:fail')
|
||||
throw new ApiError(0, message)
|
||||
}
|
||||
} finally { if (timer) clearTimeout(timer) }
|
||||
|
||||
if (response.statusCode === 401) {
|
||||
handleUnauthorized({ redirect: !url.startsWith('/banban/auth/') })
|
||||
}
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
throw new ApiError(response.statusCode, getErrorDetail(response.data))
|
||||
const body = response.data as any
|
||||
throw new ApiError(response.statusCode, getErrorDetail(body), body?.detail?.code || body?.code)
|
||||
}
|
||||
|
||||
return response.data as T
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { request } from './api'
|
||||
import { getCurrentUserId } from './session'
|
||||
|
||||
export const TRAFFIC_APP_ID = 'wx56e1cfc52aab630f'
|
||||
export type TrafficAction = 'bind' | 'unbind'
|
||||
export interface TrafficOperation {
|
||||
operation_id: string
|
||||
device_id: string
|
||||
action: TrafficAction
|
||||
status: 'pending' | 'partial_failed' | 'completed'
|
||||
traffic_status: 'pending' | 'client_confirmed' | 'server_confirmed'
|
||||
last_error?: string | null
|
||||
}
|
||||
export interface TrafficOverview {
|
||||
device_id: string
|
||||
binding_instance_id: string
|
||||
bind: TrafficOperation | null
|
||||
unbind: TrafficOperation | null
|
||||
}
|
||||
const key = () => `trafficBinding:${getCurrentUserId()}`
|
||||
type Pending = { operation: TrafficOperation; receipt?: any }
|
||||
|
||||
export function getPendingTraffic(): Pending | null {
|
||||
if (!getCurrentUserId()) return null
|
||||
const value = Taro.getStorageSync(key())
|
||||
return value?.operation?.operation_id ? value : null
|
||||
}
|
||||
export function rememberOperation(operation: TrafficOperation) {
|
||||
if (!getCurrentUserId()) return
|
||||
const previous = getPendingTraffic()
|
||||
Taro.setStorageSync(key(), { operation, receipt: previous?.operation.operation_id === operation.operation_id ? previous.receipt : undefined })
|
||||
}
|
||||
export function captureTrafficReturn(options: any): boolean {
|
||||
if (options?.referrerInfo?.appId !== TRAFFIC_APP_ID) return false
|
||||
const data = options.referrerInfo.extraData
|
||||
const pending = getPendingTraffic()
|
||||
if (!pending || !data || data.operationId !== pending.operation.operation_id
|
||||
|| data.action !== pending.operation.action || data.requestId !== `joint-${data.operationId}`
|
||||
|| typeof data.success !== 'boolean' || !/^[A-Za-z0-9_]{1,64}$/.test(data.code || '')) return false
|
||||
if (data.success && (data.code !== 'OK' || !/^[A-Z0-9]{18,22}$/.test(data.iccid || ''))) return false
|
||||
// Only store the bounded receipt, never another app's tokens or arbitrary extraData.
|
||||
Taro.setStorageSync(key(), { operation: pending.operation, receipt: {
|
||||
action: data.action, request_id: data.requestId, success: data.success, code: data.code,
|
||||
iccid: data.success ? data.iccid : null,
|
||||
} })
|
||||
return true
|
||||
}
|
||||
export function trafficLinked(overview: TrafficOverview | null): boolean {
|
||||
return Boolean(overview?.bind?.status === 'completed' && overview.bind.traffic_status === 'server_confirmed' && !overview?.unbind)
|
||||
}
|
||||
export async function getTrafficOverview(deviceId: string) {
|
||||
const user = getCurrentUserId()
|
||||
const overview = await request<TrafficOverview>(`/banban/joint-bindings/devices/${encodeURIComponent(deviceId)}`)
|
||||
assertSameUser(user)
|
||||
if (overview.bind && !overview.unbind) overview.bind = await checkTrafficOperation(overview.bind.operation_id)
|
||||
return overview
|
||||
}
|
||||
export function getTrafficOperation(operationId: string) {
|
||||
return request<TrafficOperation>(`/banban/joint-bindings/${encodeURIComponent(operationId)}`)
|
||||
}
|
||||
export async function checkTrafficOperation(operationId: string) {
|
||||
const user = getCurrentUserId()
|
||||
const operation = await request<TrafficOperation>(`/banban/joint-bindings/${encodeURIComponent(operationId)}/check`, { method: 'POST' })
|
||||
assertSameUser(user)
|
||||
return operation
|
||||
}
|
||||
export async function startTrafficOperation(deviceId: string, action: TrafficAction) {
|
||||
const user = getCurrentUserId()
|
||||
const operation = await request<TrafficOperation>('/banban/joint-bindings', { method: 'POST', data: { device_id: deviceId, action } })
|
||||
assertSameUser(user)
|
||||
rememberOperation(operation)
|
||||
return operation
|
||||
}
|
||||
export async function flushTrafficReceipt(): Promise<TrafficOperation | null> {
|
||||
const pending = getPendingTraffic()
|
||||
if (!pending?.receipt) return null
|
||||
const user = getCurrentUserId()
|
||||
let result: TrafficOperation
|
||||
try {
|
||||
result = await request<TrafficOperation>(`/banban/joint-bindings/${pending.operation.operation_id}/result`, {
|
||||
method: 'POST', data: pending.receipt,
|
||||
})
|
||||
} catch (error: any) {
|
||||
assertSameUser(user)
|
||||
if (!['STALE_OPERATION', 'OPERATION_NOT_FOUND'].includes(error.code)) throw error
|
||||
// A previous binding instance must not prevent this device from being bound again.
|
||||
if (getPendingTraffic()?.operation.operation_id === pending.operation.operation_id) Taro.setStorageSync(key(), null)
|
||||
return null
|
||||
}
|
||||
assertSameUser(user)
|
||||
if (getPendingTraffic()?.operation.operation_id === result.operation_id) Taro.setStorageSync(key(), { operation: result })
|
||||
return result
|
||||
}
|
||||
export async function resumeTrafficOperation(deviceId: string, action: TrafficAction) {
|
||||
await flushTrafficReceipt()
|
||||
const pending = getPendingTraffic()
|
||||
if (pending?.operation.device_id === deviceId && pending.operation.action === action) {
|
||||
try { return await checkTrafficOperation(pending.operation.operation_id) }
|
||||
catch (error: any) {
|
||||
if (!['STALE_OPERATION', 'OPERATION_NOT_FOUND'].includes(error.code)) throw error
|
||||
}
|
||||
}
|
||||
return startTrafficOperation(deviceId, action)
|
||||
}
|
||||
export async function finishTrafficUnbind(operation: TrafficOperation) {
|
||||
const user = getCurrentUserId()
|
||||
const result = await request<TrafficOperation>(`/banban/joint-bindings/${operation.operation_id}/finish-unbind`, { method: 'POST' })
|
||||
assertSameUser(user)
|
||||
rememberOperation(result)
|
||||
return result
|
||||
}
|
||||
function targetVersion(): 'trial' | 'release' {
|
||||
return Taro.getAccountInfoSync().miniProgram.envVersion === 'release' ? 'release' : 'trial'
|
||||
}
|
||||
function assertSameUser(user: number) {
|
||||
if (!user || getCurrentUserId() !== user) throw new Error('账号已变化,请重新进入')
|
||||
}
|
||||
async function navigate(options: any, withinApp = false) {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
return await Promise.race([withinApp ? Taro.navigateTo(options) : Taro.navigateToMiniProgram(options), new Promise((_, reject) => {
|
||||
timer = setTimeout(() => reject(new Error('跳转等待已结束,请重新进入确认')), 15000)
|
||||
})])
|
||||
} finally { if (timer) clearTimeout(timer) }
|
||||
}
|
||||
export async function jumpTraffic(operation: TrafficOperation) {
|
||||
const user = getCurrentUserId()
|
||||
const data = await request<any>(`/banban/joint-bindings/${operation.operation_id}/prepare`, { method: 'POST' })
|
||||
assertSameUser(user)
|
||||
rememberOperation(data.operation)
|
||||
if (data.operation.status === 'completed' || data.operation.traffic_status === 'server_confirmed') return data.operation as TrafficOperation
|
||||
if (data.target_app_id !== TRAFFIC_APP_ID) throw new Error('目标小程序配置不一致')
|
||||
await navigate({
|
||||
appId: TRAFFIC_APP_ID, path: 'pages/handoff/index', envVersion: targetVersion(),
|
||||
extraData: { operationId: operation.operation_id, action: operation.action,
|
||||
requestId: data.request_id, sourceBindingId: data.source_binding_id,
|
||||
handoffToken: data.handoff_token, iccid: data.iccid, deviceId: operation.device_id,
|
||||
recoveryOnly: data.recovery_only === true },
|
||||
})
|
||||
return null
|
||||
}
|
||||
export async function openTrafficPortal(deviceId: string, destination: 'device' | 'recharge') {
|
||||
const user = getCurrentUserId()
|
||||
const data = await request<any>(`/banban/joint-bindings/devices/${encodeURIComponent(deviceId)}/portal`)
|
||||
assertSameUser(user)
|
||||
if (data.target_app_id !== TRAFFIC_APP_ID) throw new Error('目标小程序配置不一致')
|
||||
await navigate({ appId: TRAFFIC_APP_ID, path: 'pages/handoff/index', envVersion: targetVersion(),
|
||||
extraData: { action: 'view', destination, iccid: data.iccid, deviceId,
|
||||
sourceBindingId: data.source_binding_id } })
|
||||
}
|
||||
export function trafficPageUrl(deviceId: string, action: TrafficAction = 'bind') {
|
||||
return `/pages/traffic-binding/index?device_id=${encodeURIComponent(deviceId)}&action=${action}`
|
||||
}
|
||||
export function openTrafficPage(deviceId: string, action: TrafficAction = 'bind') {
|
||||
return navigate({ url: trafficPageUrl(deviceId, action) }, true)
|
||||
}
|
||||
Reference in New Issue
Block a user