npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vncitizens-bridge

v1.0.1

Published

JWT bridge giữa Flutter InAppWebView và VNCitizens mini apps

Readme

vncitizens-bridge

JWT bridge chính thức giữa Flutter InAppWebViewVNCitizens mini apps.

Hoạt động với mọi framework: React, Next.js, Angular, Vue 3, PHP/Laravel, Vanilla JS.


Mục lục

  1. Kiến trúc tổng quan
  2. Yêu cầu hệ thống
  3. Phát hành bản mới (CI/CD)
  4. Tích hợp Flutter (phía app)
  5. Tích hợp mini app — React / Next.js
  6. Tích hợp mini app — Angular
  7. Tích hợp mini app — Vue 3
  8. Tích hợp mini app — PHP / Laravel
  9. Tích hợp mini app — Vanilla JS
  10. Deploy mini app
  11. Update bridge lên version mới
  12. API Reference
  13. Bảo mật
  14. Troubleshooting

1. Kiến trúc tổng quan

Flutter App
  └─ MiniAppPage (InAppWebView)
       └─ UserScript inject window.__VNCITIZENS__ = { token, platform, appVersion }
            │
            ▼
      Mini App (React / Angular / Vue / PHP / Vanilla)
        └─ vncitizens-bridge đọc token → xoá window.__VNCITIZENS__
        └─ createApiClient() — axios với auto Bearer + 401 refresh
             │
             ├─ API call 200 OK → tiếp tục
             └─ API call 401   → callHandler('refreshToken') → Flutter refresh
                                   → retry request → tiếp tục

Nguyên tắc cốt lõi:

  • Token chỉ sống trong memory — không bao giờ lưu localStorage hay sessionStorage
  • window.__VNCITIZENS__ bị xoá ngay sau khi bridge đọc xong
  • Refresh token tự động deduplicate — N request 401 cùng lúc chỉ gọi Flutter 1 lần
  • Mini app developer không cần biết gì về token — chỉ gọi api.get(...) bình thường

2. Yêu cầu hệ thống

| Thành phần | Phiên bản tối thiểu | |---|---| | Flutter | 3.10+ | | flutter_inappwebview | 6.0+ | | Node.js | 18+ | | axios (peer dep) | 1.0+ | | Vite (cho React/Vue) | 5+ |


3. Phát hành bản mới (CI/CD)

Package này được phát hành chính thức lên npmjs.com.

3.1 Đăng nhập (Lần đầu)

npm login
# Nhập username: autrungtinhnghich
# Nhập password: ... (liên hệ lead)

3.2 Build và Publish

# 1. Cài đặt dependencies
npm install

# 2. Build tất cả các bản phân phối (ESM, CJS, UMD)
npm run build

# 3. Publish lên npmjs (mặc định là public)
npm run publish:npm

3.3 Kiểm tra bản mới

npm view vncitizens-bridge


5. Tích hợp Flutter (phía app)

5.1 Thêm dependency

# pubspec.yaml
dependencies:
  flutter_inappwebview: ^6.0.0
flutter pub get

5.2 Copy 2 file Dart vào project

Đặt vào packages/web_shell/lib/src/:

packages/web_shell/lib/src/
  ├── web_view_service.dart   ← đăng ký JS handlers
  └── mini_app_page.dart      ← widget WebView wrapper

5.3 Sử dụng MiniAppPage

// Trong router hoặc bất kỳ màn hình nào
import 'package:web_shell/src/mini_app_page.dart';

// Mở mini app khi user bấm menu
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (_) => MiniAppPage(
      url: 'https://app-a.vncitizens.vn',
      title: 'Dịch vụ A',

      // Lấy token từ AuthRepository của bạn
      getToken: () => context.read<AuthRepository>().getValidToken(),

      // Refresh token khi hết hạn
      refreshToken: () => context.read<AuthRepository>().refreshToken(),

      // Mini app yêu cầu navigate sang màn hình Flutter
      onNavigate: (route, params) {
        context.go(route, extra: params); // GoRouter
      },

      // Auth error không thể recover → logout
      onAuthError: () {
        context.read<AuthBloc>().add(LogoutEvent());
      },

      // Nhận custom events từ mini app (tuỳ chọn)
      onEvent: (event, payload) {
        debugPrint('Mini app event: $event, payload: $payload');
      },
    ),
  ),
);

6. Tích hợp mini app — React / Next.js

npm install vncitizens-bridge axios npm install -D vite @vitejs/plugin-react


### 6.2 Cấu hình Vite plugin

```javascript
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { vncBridgePlugin } from 'vncitizens-bridge/vite-plugin'

export default defineConfig({
  plugins: [
    react(),
    vncBridgePlugin({
      apiBaseUrl: 'https://api-a.vncitizens.vn', // URL backend của mini app này
      appName: 'mini-app-a',                     // Tên định danh duy nhất
    }),
  ],
})

6.3 Import và dùng

// src/main.jsx
import { api, vncBridge } from 'virtual:vncitizens-bridge'
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

createRoot(document.getElementById('root')).render(
  <App api={api} bridge={vncBridge} />
)
// src/App.jsx — hoặc bất kỳ component nào
export default function App({ api, bridge }) {
  const [data, setData] = React.useState([])

  React.useEffect(() => {
    // Token tự gắn, 401 tự refresh — không cần xử lý gì thêm
    api.get('/orders').then(r => setData(r.data))
  }, [])

  return (
    <div>
      {/* Gọi navigate sang màn hình Flutter native */}
      <button onClick={() => bridge.navigate('/profile')}>
        Trang cá nhân
      </button>
    </div>
  )
}

7. Tích hợp mini app — Angular

7.1 Cài đặt

npm install vncitizens-bridge axios

7.2 Tạo Service

// src/app/core/vnc-bridge.service.ts
import { Injectable } from '@angular/core'
import { VNCitizensBridge, createApiClient } from 'vncitizens-bridge'
import { AxiosInstance } from 'axios'

@Injectable({ providedIn: 'root' })
export class VncBridgeService {
  private bridge = new VNCitizensBridge()
  readonly api: AxiosInstance

  constructor() {
    this.api = createApiClient('https://api-b.vncitizens.vn')
  }

  get isFlutter()     { return this.bridge.isFlutter }
  getToken()          { return this.bridge.getToken() }
  navigate(route: string, params = {}) {
    this.bridge.navigate(route, params)
  }
}

7.3 Đăng ký APP_INITIALIZER

// src/app/app.config.ts
import { ApplicationConfig, APP_INITIALIZER } from '@angular/core'
import { VncBridgeService } from './core/vnc-bridge.service'

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: (bridge: VncBridgeService) => () => Promise.resolve(),
      deps: [VncBridgeService],
      multi: true,
    },
  ],
}

7.4 Dùng trong component

// src/app/features/orders/orders.component.ts
import { Component, inject, OnInit } from '@angular/core'
import { VncBridgeService } from '../../core/vnc-bridge.service'

@Component({ selector: 'app-orders', templateUrl: './orders.component.html' })
export class OrdersComponent implements OnInit {
  private bridge = inject(VncBridgeService)
  orders: any[] = []

  async ngOnInit() {
    const res = await this.bridge.api.get('/orders')
    this.orders = res.data
  }
}

8. Tích hợp mini app — Vue 3

8.1 Cài đặt

npm install vncitizens-bridge axios

8.2 Tạo Vue plugin

// src/plugins/vncBridge.js
import { VNCitizensBridge, createApiClient } from 'vncitizens-bridge'

const bridge = new VNCitizensBridge()
const api    = createApiClient('https://api-c.vncitizens.vn')

export const vncBridgePlugin = {
  install(app) {
    app.provide('vncBridge', bridge)
    app.provide('api', api)
    app.config.globalProperties.$vncBridge = bridge
    app.config.globalProperties.$api       = api
  },
}

8.3 Đăng ký plugin

// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import { vncBridgePlugin } from './plugins/vncBridge'

createApp(App).use(vncBridgePlugin).mount('#app')

8.4 Dùng trong component

<!-- src/components/OrderList.vue -->
<script setup>
import { inject, onMounted, ref } from 'vue'

const api    = inject('api')
const bridge = inject('vncBridge')
const orders = ref([])

onMounted(async () => {
  const res = await api.get('/orders')
  orders.value = res.data
})
</script>

<template>
  <div>
    <button @click="bridge.navigate('/profile')">Trang cá nhân</button>
    <div v-for="order in orders" :key="order.id">{{ order.name }}</div>
  </div>
</template>

9. Tích hợp mini app — PHP / Laravel

PHP không dùng bundler nên sử dụng UMD bundle qua CDN nội bộ.

9.1 Upload UMD bundle lên CDN

# Sau khi build bridge, upload lên CDN/server
aws s3 cp dist/index.umd.js \
  s3://your-bucket/vncitizens-bridge/1.0.0/index.umd.js \
  --content-type "application/javascript" \
  --cache-control "public, max-age=31536000, immutable"

Hoặc copy vào Nginx static folder:

cp dist/index.umd.js /var/www/cdn/vncitizens-bridge/1.0.0/index.umd.js

9.2 Thêm vào layout chính

<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html>
<head>
  <!-- 1. axios -->
  <script src="https://cdn.jsdelivr.net/npm/axios@1/dist/axios.min.js"></script>

  <!-- 2. VNCitizens Bridge UMD -->
  <script src="https://cdn.vncitizens.vn/vncitizens-bridge/1.0.0/index.umd.js"></script>

  <!-- 3. Khởi tạo bridge -->
  <script>
    const { vncBridge, createApiClient } = window.VNCitizensBridgeSDK
    window.__VNC_BRIDGE__ = vncBridge
    window.__VNC_API__    = createApiClient('https://api-d.vncitizens.vn')
  </script>
</head>
<body>
  @yield('content')
  @stack('scripts')
</body>
</html>

9.3 Dùng trong view

<!-- resources/views/orders/index.blade.php -->
@extends('layouts.app')

@section('content')
<div id="orders-list"></div>
@endsection

@push('scripts')
<script>
  window.__VNC_API__.get('/orders').then(res => {
    document.getElementById('orders-list').innerHTML =
      res.data.map(o => `<div class="order">${o.name}</div>`).join('')
  }).catch(console.error)
</script>
@endpush

10. Tích hợp mini app — Vanilla JS

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <script src="https://cdn.jsdelivr.net/npm/axios@1/dist/axios.min.js"></script>
  <script src="https://cdn.vncitizens.vn/vncitizens-bridge/1.0.0/index.umd.js"></script>
</head>
<body>
  <div id="app"></div>

  <script type="module">
    const { vncBridge, createApiClient } = window.VNCitizensBridgeSDK
    const api = createApiClient('https://api-e.vncitizens.vn')

    // Token đã tự gắn — gọi API bình thường
    const res = await api.get('/data')
    document.getElementById('app').textContent = JSON.stringify(res.data)
  </script>
</body>
</html>

11. Deploy mini app

11.1 Thêm scripts vào package.json

{
  "scripts": {
    "dev":                "vite",
    "build":              "vite build",
    "deploy:staging":     "npm run build && bash scripts/deploy.sh staging",
    "deploy:production":  "npm run build && bash scripts/deploy.sh production"
  }
}

11.2 Script deploy

Tạo file scripts/deploy.sh:

#!/bin/bash
set -e

ENV=${1:-staging}
APP_NAME=$(node -p "require('./package.json').name")
BUILD_DIR="dist"

echo "🚀 Deploying $APP_NAME to $ENV..."

if [ "$ENV" = "production" ]; then
  BUCKET="s3://your-bucket/apps/$APP_NAME"
  CLOUDFRONT_ID="XXXXXXXXXXXXX"

  # Upload static assets (cache vĩnh viễn)
  aws s3 sync $BUILD_DIR $BUCKET \
    --delete \
    --exclude "index.html" \
    --cache-control "public, max-age=31536000, immutable"

  # Upload index.html (không cache — để update ngay)
  aws s3 cp $BUILD_DIR/index.html $BUCKET/index.html \
    --cache-control "no-cache, no-store, must-revalidate"

  # Invalidate CloudFront
  aws cloudfront create-invalidation \
    --distribution-id $CLOUDFRONT_ID \
    --paths "/$APP_NAME/*"

else
  # Staging: rsync lên server
  rsync -avz --delete $BUILD_DIR/ \
    [email protected]:/var/www/apps/$APP_NAME/
fi

echo "✅ Done. Live: https://$APP_NAME.vncitizens.vn"

11.3 Chạy deploy

# Staging
npm run deploy:staging

# Production
npm run deploy:production

12. Update bridge lên version mới

# 1. Tại thư mục vncitizens-bridge:
# Tăng version theo chuẩn Semantic Versioning
npm version patch # 1.0.0 -> 1.0.1
# Build và publish
npm run publish:npm

# 2. Tại thư mục mini app:
npm update vncitizens-bridge
npm run build
# Deploy lại mini app
npm run deploy:production

13. API Reference

VNCitizensBridge

class VNCitizensBridge {
  constructor(options?: {
    /** Danh sách origin được phép (dành cho bảo mật tương lai) */
    allowedOrigins?: string[]
  })

  /** true nếu đang chạy trong Flutter WebView */
  readonly isFlutter: boolean

  /** Platform string từ Flutter ("flutter") */
  readonly platform: string | null

  /** Version của Flutter app */
  readonly appVersion: string | null

  /** Lấy JWT token hiện tại (đồng bộ) */
  getToken(): string | null

  /**
   * Yêu cầu Flutter refresh JWT.
   * Tự động dedup — N lần gọi đồng thời chỉ tạo 1 request.
   */
  refresh(): Promise<string>

  /** Yêu cầu Flutter navigate sang màn hình native */
  navigate(route: string, params?: object): void

  /** Gửi custom event lên Flutter */
  emit(event: string, payload?: object): void

  /** 
   * Đăng ký lắng nghe khi token thay đổi (sau khi refresh).
   * Trả về hàm cleanup.
   */
  onTokenUpdated(callback: (token: string) => void): () => void

  /** Cập nhật JWT token thủ công (dành cho app tự quản lý auth) */
  setToken(token: string | null): void
}

// Singleton — dùng chung toàn app
export const vncBridge: VNCitizensBridge

createApiClient(baseURL, options?, bridge?)

function createApiClient(
  baseURL: string,
  options?: AxiosRequestConfig & { attachToken?: boolean },
  bridge?: VNCitizensBridge
): AxiosInstance

Tạo axios instance với:

  • Request interceptor: tự gắn Authorization: Bearer <token> (nếu attachToken !== false)
  • Response interceptor: bắt 401 → gọi bridge.refresh() → retry 1 lần

vncBridgePlugin(options) (Vite only)

function vncBridgePlugin(options: {
  apiBaseUrl: string  // URL backend của mini app
  appName: string     // Tên định danh (kebab-case)
}): Plugin

Cung cấp virtual module virtual:vncitizens-bridge export { api, vncBridge }.

JS Handlers (Flutter phải đăng ký)

| Handler | Mô tả | Return | |---|---|---| | getToken | Lấy token hiện tại | { token, error } | | refreshToken | Refresh JWT | { token, error } | | onAuthError | Auth error không thể recover | null | | navigate | Navigate màn hình Flutter | null | | onEvent | Custom event từ mini app | null |

window.__VNCITIZENS__ (inject từ Flutter)

interface VNCitizensContext {
  token: string        // JWT access token
  platform: string     // "flutter"
  appVersion: string   // Flutter app version
  timestamp: number    // Unix timestamp lúc inject
}

Quản lý Token thủ công (Tùy chọn)

Nếu bạn không muốn dùng createApiClient mặc định mà muốn tự quản lý token (ví dụ: đưa vào Redux/Pinia hoặc dùng fetch), bạn có thể sử dụng các API sau:

1. Lấy token và lắng nghe thay đổi

import { vncBridge } from 'vncitizens-bridge'

// Lấy token hiện tại
const token = vncBridge.getToken()

// Lắng nghe khi Flutter refresh token mới
vncBridge.onTokenUpdated((newToken) => {
  console.log('Token mới:', newToken)
  // Cập nhật vào store của bạn (Redux, Pinia, v.v.)
  store.dispatch(setToken(newToken))
})

// Cập nhật token thủ công (Ví dụ: sau khi user login qua API riêng của mini app)
vncBridge.setToken('ey...xyz')

2. Dùng với Axios riêng (Tự gắn header)

import { createApiClient, vncBridge } from 'vncitizens-bridge'

const api = createApiClient('https://api.example.com', {
  attachToken: false // Tắt tự động gắn Authorization header
})

// Tự gắn interceptor theo ý mình
api.interceptors.request.use(config => {
  const token = vncBridge.getToken()
  config.headers['X-Custom-Auth'] = `Bearer ${token}`
  return config
})

3. Dùng với Native Fetch

import { vncBridge } from 'vncitizens-bridge'

async function fetchData(url) {
  let token = vncBridge.getToken()
  
  let response = await fetch(url, {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  })

  // Nếu bị 401, thử refresh và gọi lại
  if (response.status === 401 && vncBridge.isFlutter) {
    token = await vncBridge.refresh()
    response = await fetch(url, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    })
  }

  return response.json()
}

14. Bảo mật

| Quy tắc | Lý do | |---|---| | Không lưu token vào localStorage | XSS có thể đọc và đánh cắp | | Xoá window.__VNCITIZENS__ ngay sau khi đọc | Giảm thời gian tồn tại trên window | | Dùng AT_DOCUMENT_START | Đảm bảo token có trước khi bất kỳ script nào chạy | | Validate origin trước khi gửi token | Chặn cross-origin token leak | | Token là short-lived JWT (15–30 phút) | Giới hạn damage nếu bị leak | | Retry chỉ 1 lần (_vncRetried flag) | Tránh refresh loop vô tận | | Mini app chỉ chạy trên HTTPS | Bắt buộc cho production |


15. Troubleshooting

Token bị hết hạn nhưng không refresh

Kiểm tra:

  1. Flutter đã đăng ký handler refreshToken chưa? (WebViewService.registerHandlers())
  2. AuthRepository.refreshToken() có throw exception không?
  3. Backend có trả đúng 401 (không phải 403) không?

Nhiều request 401 cùng lúc gây nhiều lần refresh

Bridge đã xử lý tự động bằng #refreshPromise dedup. Nếu vẫn xảy ra, kiểm tra xem có đang dùng nhiều instance VNCitizensBridge hay không — chỉ nên dùng 1 singleton vncBridge.


License

MIT © VNCitizens Team