PAYATHON 2026

如何从 Samsung Tizen TV browser 回调 app?

支付阿杰

结论

不能把 Samsung Tizen TV Browser 当成可以直接回调 Web App 的原生 WebView。浏览器和 Tizen Web App 通常运行在相互隔离的上下文中,第三方支付页还会受到同源策略限制。App 因此无法直接读取支付页面的状态,也不能稳定接收 JavaScript 回调。

更稳妥的方案是:

  1. App 向业务服务端创建支付订单,获取唯一的 orderId。
  2. 支付页面携带该订单号。
  3. 支付平台通过服务端 webhook 或 notify URL 通知业务服务端。
  4. Tizen App 通过轮询或 WebSocket 查询订单状态。
  5. 如果支付页确实位于 App 创建的子窗口或 iframe 中,可以在页面回跳后用 postMessage() 辅助通知 App,但不能把它作为判断支付结果的唯一依据。

推荐架构

Tizen Web App
      │
      ├── 创建订单 ──────────────> 业务服务端
      │                              │
      │                              ├── 调用支付 API
      │                              │
      │                              <── 支付平台 webhook
      │
      └── 轮询或 WebSocket ──────> 查询最终支付状态

支付是否成功,必须由服务端根据支付平台的签名通知或订单查询结果来确认。浏览器跳转参数只能用于改善交互,不能作为可信的支付凭证。

实现步骤

1. 服务端创建支付订单

App 应先调用自己的服务端,不要在前端保存支付平台密钥或生成签名。

async function createPayment() {
  const response = await fetch('https://api.example.com/payments', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      productId: 'premium-plan'
    })
  });

  if (!response.ok) {
    throw new Error('Failed to create payment');
  }

  return response.json();
}

服务端可以返回以下内容:

{
  "orderId": "ORDER_20260901_001",
  "paymentUrl": "https://payment.example.com/checkout/abc123",
  "expiresAt": "2026-09-01T12:30:00Z"
}

可以把 paymentUrl 显示为二维码,让用户通过手机完成支付。如果目标电视型号和支付平台支持,也可以尝试在新窗口中打开支付页面。

const payment = await createPayment();

const paymentWindow = window.open(
  payment.paymentUrl,
  'payment',
  'width=960,height=540'
);

if (!paymentWindow) {
  // 新窗口不受支持时,改为显示二维码或提示用户使用手机支付。
}

不同 Samsung TV 型号、Tizen 版本和应用运行方式,对弹窗、外部导航及窗口控制的支持可能不同。上线前需要在目标真机上验证。

2. 支付平台通知业务服务端

在支付平台后台配置 webhook、notify URL 或类似的异步通知地址:

POST https://api.example.com/payment-callback

服务端收到通知后,应执行以下操作:

  • 验证支付平台签名。
  • 核对商户号、订单号、金额和币种。
  • 避免重复处理同一条通知。
  • 必要时主动调用支付平台的订单查询 API。
  • 将订单状态更新为 PAID、FAILED 或其他业务状态。

不能只根据回跳 URL 中的 status=success 判断支付成功。

3. Tizen App 轮询订单状态

轮询的兼容性较好,不依赖浏览器能否唤醒 App。

async function waitForPayment(orderId) {
  const intervalMs = 3000;
  const timeoutMs = 10 * 60 * 1000;
  const startedAt = Date.now();

  while (Date.now() - startedAt < timeoutMs) {
    const response = await fetch(
      `https://api.example.com/payments/${encodeURIComponent(orderId)}`,
      {
        method: 'GET',
        cache: 'no-store'
      }
    );

    if (!response.ok) {
      throw new Error('Failed to query payment status');
    }

    const result = await response.json();

    if (result.status === 'PAID') {
      return result;
    }

    if (result.status === 'FAILED' || result.status === 'CANCELLED') {
      throw new Error(`Payment ended with status: ${result.status}`);
    }

    await new Promise(resolve => setTimeout(resolve, intervalMs));
  }

  throw new Error('Payment status check timed out');
}

调用方式:

const payment = await createPayment();

try {
  const result = await waitForPayment(payment.orderId);
  showPaymentSuccess(result);
} catch (error) {
  showPaymentError(error.message);
}

如果需要更及时地更新状态,可以使用 WebSocket 或 Server-Sent Events,同时保留订单查询接口,以便在连接中断后恢复查询。

同一窗口链路下使用 postMessage()

如果支付页面由 window.open() 打开,并且支付完成后可以重定向到你控制的 HTTPS 页面,例如:

https://app.example.com/payment/return?orderId=ORDER_20260901_001

回跳页可以向打开它的页面发送通知:

<script>
  const params = new URLSearchParams(window.location.search);
  const orderId = params.get('orderId');

  if (window.opener) {
    window.opener.postMessage(
      {
        type: 'PAYMENT_RETURNED',
        orderId
      },
      'https://app.example.com'
    );
  }

  window.close();
</script>

App 侧接收消息:

window.addEventListener('message', async event => {
  if (event.origin !== 'https://app.example.com') {
    return;
  }

  const message = event.data;

  if (
    !message ||
    message.type !== 'PAYMENT_RETURNED' ||
    typeof message.orderId !== 'string'
  ) {
    return;
  }

  // postMessage 只说明页面已回跳,最终状态仍从服务端查询。
  const response = await fetch(
    `https://api.example.com/payments/${encodeURIComponent(message.orderId)}`,
    { cache: 'no-store' }
  );

  const payment = await response.json();

  if (payment.status === 'PAID') {
    showPaymentSuccess(payment);
  }
});

这种方式需要满足以下条件:

  • 支付平台允许配置回跳地址。
  • 回跳发生在 App 打开的同一个子窗口中。
  • 浏览器没有清除或隔离 window.opener。
  • Tizen TV 的窗口实现支持 postMessage() 和 window.close()。
  • targetOrigin 和 event.origin 必须使用明确的 HTTPS Origin,不能写成 *。

如果支付流程在独立的 Samsung Browser 中完成,浏览器进程和 App 之间没有 window.opener 关系,这种方案通常无法生效。

iframe 场景

跨域 iframe 受浏览器同源策略限制。父页面不能读取第三方支付页的 DOM、URL 或 JavaScript 变量。只有以下两种情况可以通信:

  • 支付平台明确提供 iframe SDK 和事件回调。
  • 支付完成后,iframe 跳转到你控制的页面,再由该页面调用 window.parent.postMessage()。
window.parent.postMessage(
  {
    type: 'PAYMENT_RETURNED',
    orderId: 'ORDER_20260901_001'
  },
  'https://app.example.com'
);

许多支付页面还会通过 X-Frame-Options 或 CSP 的 frame-ancestors 禁止 iframe 嵌入,因此不能预设第三方支付页一定能在电视 App 中加载。

不建议依赖自定义 URL Scheme

理论上可以使用自定义 URI 或 Tizen Application Control 唤起应用,但能否用于 Samsung TV、目标 Tizen 版本和普通 Browser,需要根据对应型号的官方能力并通过真机测试确认。第三方支付平台也不一定接受非 HTTP/HTTPS 的回跳地址。

即使自定义 URI 能够成功唤起 App,其中携带的参数也不能直接用于判断支付成功。App 被唤起后,仍需向服务端查询订单状态。

注意事项

  • 优先考虑让电视显示二维码,由用户在手机上完成支付。相比使用电视遥控器操作支付流程,这种方式通常更稳定。
  • 支付密钥、签名密钥和 webhook 验签逻辑只能放在服务端。
  • 每次支付都应生成不可预测的订单号,并与当前用户和商品绑定。
  • webhook 必须支持幂等处理,因为支付平台可能重复发送通知。
  • 轮询需要设置间隔和超时时间,并在页面退出时清理,避免无限请求。
  • App 重新启动后,应能通过尚未完成的 orderId 恢复订单查询。
  • postMessage()、回跳页和深度链接只用于界面通知,最终结果以服务端订单状态为准。
  • 上线前需要在实际目标电视型号上测试弹窗、焦点切换、返回键、网络中断,以及支付完成后 App 被挂起或重启等情况。

备注:内容仅供参考。