npm package: @sohu-bpd/wechat
Description: WeChat JS-SDK utility library
Latest version: 0.2.0

# @sohu-bpd/wechat

WeChat JS-SDK utility library with initialization helpers and SPA support.

## Features

- 🔄 **SPA Support** - Auto re-initialization on URL changes for single-page apps
- 📱 **iOS Compatible** - Handles iOS WeChat signature URL quirks automatically
- 🎯 **High-level APIs** - Simple `config()` and `share()` APIs for common tasks
- 🛡️ **Non-WeChat Safe** - All APIs silently return in non-WeChat environments without errors
- 📦 **TypeScript** - Full type definitions included
- 🌐 **Multiple Formats** - ESM, CommonJS, and IIFE bundles

## Installation

```bash
npm install @sohu-bpd/wechat
# or
pnpm add @sohu-bpd/wechat
# or
yarn add @sohu-bpd/wechat
```

## Quick Start

### 1. Load WeChat JS-SDK

First, include the WeChat JS-SDK script in your HTML:

```html
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
```

### 2. Configure (Optional on Sohu Domains)

**Option A: Zero Configuration (Sohu Domains Only)**

If you're on `https://act.go.sohu.com` or `https://test.r.ads.sohu.com`, you can skip configuration entirely:

```typescript
import { share } from '@sohu-bpd/wechat'

// No config needed! Just use it directly
await share({
  title: 'My Page Title',
  desc: 'Share description',
  imgUrl: 'https://example.com/image.jpg',
})
```

**Option B: Custom Configuration (Sohu Domains)**

Or customize settings on supported domains:

```typescript
import { config } from '@sohu-bpd/wechat'

// No need to provide getSignature on supported domains
config({
  debug: false,
  spa: true, // Enable SPA mode (default)
})
```

**Option C: Custom Signature Function (Other Domains)**

For other domains or custom backends:

```typescript
import { config } from '@sohu-bpd/wechat'

config({
  getSignature: async (url) => {
    // Fetch signature from your backend
    const response = await fetch(`/api/wechat/signature?url=${encodeURIComponent(url)}`)
    return response.json()
  },
  debug: false,
  spa: true, // Enable SPA mode (default)
})
```

### 3. Use Anywhere

```typescript
import { share } from '@sohu-bpd/wechat'

// Set share content
await share({
  title: 'My Page Title',
  desc: 'Share description',
  imgUrl: 'https://example.com/image.jpg',
  link: 'https://example.com/page', // optional, defaults to current URL
})
```

## Usage

### ESM / CommonJS

```typescript
import { config, share } from '@sohu-bpd/wechat'

// or
const { config, share } = require('@sohu-bpd/wechat')
```

### Browser (IIFE)

```html
<!-- Load WeChat JS-SDK first -->
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<!-- Then load this library -->
<script src="https://unpkg.com/@sohu-bpd/wechat/dist/index.global.js"></script>
<script>
  const { config, share } = SohuWechat

  config({
    getSignature: async (url) => {
      const res = await fetch('/api/wechat/signature?url=' + encodeURIComponent(url))
      return res.json()
    }
  })

  share({
    title: 'My Page',
    desc: 'Description',
    imgUrl: 'https://example.com/image.jpg'
  })
</script>
```

## API Reference

### High-level APIs

#### config(options)

Set global configuration for WeChat JS-SDK.

**Note:**
- You must load the WeChat JS-SDK script in your HTML before using this library
- **On `https://act.go.sohu.com` or `https://test.r.ads.sohu.com`, calling `config()` is optional** - the library will auto-configure with defaults when you use features like `share()`

```typescript
interface GlobalConfig {
  /**
   * Function to fetch signature, receives current URL
   * - Optional on https://act.go.sohu.com or https://test.r.ads.sohu.com
   * - Will auto-use built-in signature (requests /wechat/ticket) if not provided
   * - Required on other domains
   */
  getSignature?: (url: string) => Promise<SignatureResult>
  /** Enable debug mode, default false */
  debug?: boolean
  /** Enable SPA mode (auto re-init on URL change), default true */
  spa?: boolean
}

function config(options?: GlobalConfig): void
```

**Example - No configuration needed (Sohu domains):**

```typescript
import { share } from '@sohu-bpd/wechat'

// Just use it directly!
await share({
  title: 'My Page',
  desc: 'Description',
  imgUrl: 'https://example.com/image.jpg',
})
```

**Example - Custom signature function:**

```typescript
config({
  getSignature: async (url) => {
    const res = await fetch(`/api/wechat/signature?url=${encodeURIComponent(url)}`)
    return res.json()
  },
  debug: process.env.NODE_ENV === 'development',
  spa: true,
})
```

**Example - Custom settings with built-in signature (Sohu domains):**

```typescript
// On https://act.go.sohu.com or https://test.r.ads.sohu.com
config({
  debug: true,
  spa: true,
})
```

#### share(options)

Set WeChat share content for both friends and moments.

```typescript
interface ShareOptions {
  /** Share title */
  title: string
  /** Share description (for friends) */
  desc?: string
  /** Share link, defaults to current URL */
  link?: string
  /** Share image URL */
  imgUrl: string
}

function share(options: ShareOptions): Promise<void>
```

**Example:**

```typescript
await share({
  title: 'Amazing Product',
  desc: 'Check out this amazing product!',
  imgUrl: 'https://example.com/product.jpg',
  link: 'https://example.com/product/123',
})
```

#### getConfig()

Get current global configuration (throws if not configured).

```typescript
function getConfig(): GlobalConfig
```

#### isConfigured()

Check if global configuration has been set.

```typescript
function isConfigured(): boolean
```

### Low-level APIs

For advanced use cases, you can use the low-level APIs:

#### isWechat()

Check if the current environment is WeChat browser.

```typescript
function isWechat(): boolean
```

**Example:**

```typescript
import { isWechat, share } from '@sohu-bpd/wechat'

// Conditionally show share UI only in WeChat
if (isWechat()) {
  showShareButton()
}

// Or just call share() - it will silently do nothing outside WeChat
await share({
  title: 'My Page',
  imgUrl: 'https://example.com/image.jpg',
})
```

#### initSDK()

Initialize WeChat SDK (automatically called by high-level APIs).

**Note:** Requires WeChat JS-SDK to be loaded globally first. Silently returns in non-WeChat environments.

```typescript
function initSDK(): Promise<void>
```

#### getWx()

Get the global `wx` object after SDK is loaded. Returns `null` in non-WeChat environments or if `wx` is not available.

```typescript
function getWx(): WxSDK | null
```

#### resetSDK()

Reset SDK state (useful for testing or manual re-initialization).

```typescript
function resetSDK(): void
```

### Original init Function

For use cases that don't need global configuration.

**Note:** Requires WeChat JS-SDK to be loaded globally first.

```typescript
interface InitConfig {
  /** Public account appId */
  appId: string
  /** Signature timestamp */
  timestamp: number | string
  /** Signature nonce string */
  nonceStr: string
  /** Signature */
  signature: string
  /** JS API list to use */
  jsApiList: string[]
  /** Enable debug mode */
  debug?: boolean
}

function init(config: InitConfig): Promise<void>
```

## SPA Mode

By default, `spa: true` is enabled, which means:

- On the **first** API call (e.g., `share()`), the SDK is initialized with the current URL
- On **subsequent** API calls, if the URL has changed, the SDK is automatically re-initialized
- This is essential for single-page applications where the URL changes without page reloads

To disable SPA mode:

```typescript
config({
  // ...
  spa: false, // Only initialize once
})
```

## Built-in Signature Support

On specific Sohu domains, the library provides built-in signature functionality:

### Supported Domains

- `https://act.go.sohu.com`
- `https://test.r.ads.sohu.com`

### How It Works

When you call `config()` without providing `getSignature` on a supported domain:

1. The library automatically detects the current domain
2. Uses the built-in signature function that requests `/wechat/ticket`
3. Converts the API response to the standard `SignatureResult` format

### API Request Format

The built-in signature function sends a GET request to:

```
/wechat/ticket?url={encoded_current_url}
```

**Parameters:**
- `url` (query parameter): The current page URL (URL-encoded)

**Example Request:**
```
GET /wechat/ticket?url=https%3A%2F%2Fact.go.sohu.com%2Fpage%3Fid%3D123
```

### API Response Format

The built-in signature endpoint `/wechat/ticket` should return:

```typescript
{
  code: number          // 0 for success
  data: {
    app_id: string      // Converted to appId
    timestamp: number   // Kept as timestamp
    nonce_str: string   // Converted to nonceStr
    signature: string   // Kept as signature
  }
  message: string       // Error message if code !== 0
}
```

**Notes:**
- Uses `fetch` API if available (modern browsers)
- Automatically falls back to `XMLHttpRequest` in older environments
- Response format is automatically converted to match `SignatureResult` interface

### Usage

**Zero Configuration (Recommended):**

```typescript
import { share } from '@sohu-bpd/wechat'

// No config() needed on supported domains!
await share({
  title: 'My Page',
  desc: 'Description',
  imgUrl: 'https://example.com/image.jpg',
})
```

**With Custom Settings:**

```typescript
import { config, share } from '@sohu-bpd/wechat'

// Optional: customize settings
config({
  debug: true,
  spa: true,
})

// Then use normally
await share({
  title: 'My Page',
  desc: 'Description',
  imgUrl: 'https://example.com/image.jpg',
})
```

### Fallback to Custom Function

Even on supported domains, you can provide your own `getSignature` function:

```typescript
config({
  getSignature: async (url) => {
    // Your custom implementation
    return await fetchSignatureFromCustomAPI(url)
  },
})
```

## Custom Domain Configuration

For domains not in the built-in list, or to customize built-in domains, use the `domainConfigs` option:

### Basic Usage

```typescript
import { config } from '@sohu-bpd/wechat'

config({
  domainConfigs: {
    'https://your-domain.com': {
      endpoint: '/api/wechat/signature',
      urlParamKey: 'url',
    }
  }
})
```

### Advanced: Custom Response Format

If your backend returns a different response format:

```typescript
config({
  domainConfigs: {
    'https://your-domain.com': {
      endpoint: '/api/sign',
      urlParamKey: 'pageUrl',
      transformResponse: (response) => {
        // Convert your custom format to standard SignatureResult
        return {
          appId: response.appid,      // Map from your field names
          timestamp: response.ts,
          nonceStr: response.nonce,
          signature: response.sign
        }
      }
    }
  }
})
```

### Configuration Priority

The library uses the following priority order (highest to lowest):

1. **Custom `getSignature` function** - Complete override
2. **`domainConfigs` for current domain** - Per-domain customization
3. **Built-in default configuration** - For Sohu domains

**Example - Priority demonstration:**

```typescript
// Priority 1: getSignature always takes precedence
config({
  getSignature: async (url) => {
    // This will be used, domainConfigs ignored
    return await fetchFromCustomAPI(url)
  },
  domainConfigs: {
    'https://act.go.sohu.com': {
      endpoint: '/ignored'  // This won't be used
    }
  }
})

// Priority 2: domainConfigs overrides built-in defaults
config({
  domainConfigs: {
    'https://act.go.sohu.com': {
      endpoint: '/custom/ticket',  // Overrides default /wechat/ticket
      urlParamKey: 'targetUrl'     // Overrides default 'url'
    }
  }
})

// Priority 3: Built-in defaults (no config needed)
// On https://act.go.sohu.com or https://test.r.ads.sohu.com
await share({
  title: 'My Page',
  imgUrl: 'https://example.com/image.jpg'
})
```

### Configuration Options

| Option | Type | Required | Default | Description |
|--------|------|:--------:|---------|-------------|
| endpoint | string | No | '/wechat/ticket' | Signature API endpoint path |
| urlParamKey | string | No | 'url' | Query parameter name for URL |
| transformResponse | function | No | Built-in transform | Response format converter |

### Backend API Requirements

Your signature endpoint should:

1. Accept the page URL as a query parameter (default key: `url`)
2. Return signature data in one of these formats:

**Option A: Use built-in format (no transformResponse needed):**
```json
{
  "code": 0,
  "data": {
    "app_id": "wx123456",
    "timestamp": 1234567890,
    "nonce_str": "abc123",
    "signature": "sha1signature"
  },
  "message": "success"
}
```

**Option B: Use custom format with transformResponse:**
```json
{
  "appid": "wx123456",
  "ts": 1234567890,
  "nonce": "abc123",
  "sign": "sha1signature"
}
```

With config:
```typescript
transformResponse: (res) => ({
  appId: res.appid,
  timestamp: res.ts,
  nonceStr: res.nonce,
  signature: res.sign
})
```

## Non-WeChat Environment Safety

All APIs are safe to call in non-WeChat environments (desktop browsers, other mobile browsers, SSR, etc.). They will silently return without throwing errors, so your application works normally:

```typescript
import { share, init, initSDK, isWechat } from '@sohu-bpd/wechat'

// These all work safely in any environment:

// share() resolves immediately in non-WeChat
await share({
  title: 'My Page',
  imgUrl: 'https://example.com/image.jpg',
})

// init() resolves immediately in non-WeChat
await init({
  appId: 'wx123',
  timestamp: 123,
  nonceStr: 'abc',
  signature: 'sig',
  jsApiList: [],
})

// initSDK() resolves immediately in non-WeChat
await initSDK()

// Use isWechat() to conditionally show WeChat-specific UI
if (isWechat()) {
  showWeChatShareButton()
}
```

This design allows you to integrate WeChat sharing without wrapping every call in environment checks.

## iOS WeChat Quirk

On iOS WeChat, the signature URL should be the **first entry URL** of the app, not the current URL. This library handles this automatically:

- On iOS, it remembers the first URL and always uses it for signatures
- On Android, it uses the current URL for each signature request

You don't need to do anything special - it just works.

## Backend Signature Endpoint

Your backend should provide an endpoint that generates WeChat JS-SDK signatures. Here's an example implementation:

```typescript
// Node.js + Express example
app.get('/api/wechat/signature', async (req, res) => {
  const url = req.query.url as string

  // Generate signature using WeChat's algorithm
  const signature = await generateWeChatSignature(url)

  res.json({
    appId: 'your-app-id',
    timestamp: signature.timestamp,
    nonceStr: signature.nonceStr,
    signature: signature.signature,
  })
})
```

Refer to [WeChat JS-SDK Documentation](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html) for signature generation details.

## TypeScript Support

Full TypeScript definitions are included:

```typescript
import type {
  GlobalConfig,
  SignatureResult,
  ShareOptions
} from '@sohu-bpd/wechat'

const config: GlobalConfig = {
  sdkUrl: '...',
  getSignature: async (url: string): Promise<SignatureResult> => {
    // ...
  }
}
```

## Bundle Formats

- **ESM** (`.mjs`) - Modern JavaScript modules
- **CommonJS** (`.js`) - Node.js and bundlers
- **IIFE** (`.global.js`) - Browser `<script>` tag with global `SohuWechat`

## Changelog

### 0.0.6 (TBD)

- **Non-WeChat Safe:** All APIs now silently return in non-WeChat environments without throwing errors
- **New API:** Added `isWechat()` function to check if running in WeChat browser
- **Breaking Change:** `getWx()` now returns `WxSDK | null` instead of throwing errors
- `share()`, `init()`, and `initSDK()` resolve immediately in non-WeChat environments
- This allows seamless integration without environment checks around every API call

### 0.0.5

- **New Feature:** Built-in signature support for Sohu domains
- **Zero Configuration Mode:** On `https://act.go.sohu.com` and `https://test.r.ads.sohu.com`, you can now use the library without calling `config()` at all
- `getSignature` is now optional on supported domains
- Automatic signature fetching from `/wechat/ticket?url={current_url}` endpoint on supported domains
- Built-in request uses `fetch` API with automatic fallback to `XMLHttpRequest` for older browsers
- Added comprehensive tests for built-in signature functionality, xhr fallback, and auto-initialization

### 0.0.3 (2026-01-29)

- **Breaking Change:** Removed automatic SDK loading functionality
- Users must now manually include the WeChat JS-SDK script in their HTML
- Removed `sdkUrl` configuration option from `GlobalConfig`
- Removed `loadSDK()` export from public API
- Updated documentation with instructions for manual SDK inclusion

### 0.0.2 (2026-01-29)

- Extract WeChat JS-SDK module as standalone package
- High-level `config()` and `share()` APIs
- Automatic SDK loading and initialization
- SPA mode with auto re-initialization on URL changes
- iOS WeChat signature URL handling
- Full TypeScript support
- Comprehensive unit tests
- Multiple bundle formats (ESM, CJS, IIFE)

## License

MIT