161 changed files with 12196 additions and 11612 deletions
@ -1,32 +0,0 @@ |
|||
module.exports = { |
|||
root: true, |
|||
env: { |
|||
node: true, |
|||
es2022: true, |
|||
}, |
|||
extends: [ |
|||
"plugin:vue/vue3-recommended", |
|||
"eslint:recommended", |
|||
"@vue/typescript/recommended", |
|||
"plugin:prettier/recommended" |
|||
], |
|||
// parserOptions: {
|
|||
// ecmaVersion: 2020,
|
|||
// },
|
|||
rules: { |
|||
"max-len": ["warn", { |
|||
code: 100, |
|||
ignoreComments: true, |
|||
ignorePattern: '^\\s*class="[^"]*"$', |
|||
ignoreStrings: true, |
|||
ignoreTemplateLiterals: true, |
|||
ignoreUrls: true, |
|||
}], |
|||
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn", |
|||
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn", |
|||
"@typescript-eslint/no-explicit-any": "warn", |
|||
"@typescript-eslint/explicit-function-return-type": "off", |
|||
"@typescript-eslint/no-unnecessary-type-constraint": "off", |
|||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] |
|||
}, |
|||
}; |
|||
@ -0,0 +1,57 @@ |
|||
{ |
|||
"root": true, |
|||
"env": { |
|||
"node": true, |
|||
"browser": true, |
|||
"es2022": true |
|||
}, |
|||
"extends": [ |
|||
"plugin:vue/vue3-recommended", |
|||
"eslint:recommended", |
|||
"@vue/typescript/recommended", |
|||
"plugin:prettier/recommended" |
|||
], |
|||
"parser": "vue-eslint-parser", |
|||
"parserOptions": { |
|||
"parser": "@typescript-eslint/parser", |
|||
"ecmaVersion": 2022, |
|||
"sourceType": "module", |
|||
"extraFileExtensions": [".vue"], |
|||
"ecmaFeatures": { |
|||
"jsx": true |
|||
} |
|||
}, |
|||
"plugins": [ |
|||
"@typescript-eslint", |
|||
"vue", |
|||
"prettier" |
|||
], |
|||
"rules": { |
|||
"no-console": "warn", |
|||
"no-debugger": "warn", |
|||
"@typescript-eslint/no-explicit-any": "off", |
|||
"vue/multi-word-component-names": "off", |
|||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], |
|||
"@typescript-eslint/no-unnecessary-type-constraint": "off", |
|||
"vue/no-parsing-error": ["error", { |
|||
"x-invalid-end-tag": false, |
|||
"invalid-first-character-of-tag-name": false |
|||
}], |
|||
"vue/no-v-html": "warn", |
|||
"prettier/prettier": ["error", { |
|||
"singleQuote": true, |
|||
"semi": false, |
|||
"trailingComma": "none" |
|||
}] |
|||
}, |
|||
"overrides": [ |
|||
{ |
|||
"files": ["*.ts", "*.tsx", "*.mts"], |
|||
"parser": "@typescript-eslint/parser" |
|||
}, |
|||
{ |
|||
"files": ["*.js", "*.jsx", "*.mjs"], |
|||
"parser": "@typescript-eslint/parser" |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
export default { |
|||
root: true, |
|||
env: { |
|||
node: true, |
|||
browser: true, |
|||
es2022: true |
|||
}, |
|||
extends: [ |
|||
'plugin:vue/vue3-recommended', |
|||
'eslint:recommended', |
|||
'@vue/typescript/recommended' |
|||
], |
|||
parser: 'vue-eslint-parser', |
|||
parserOptions: { |
|||
parser: { |
|||
'ts': '@typescript-eslint/parser', |
|||
'js': '@typescript-eslint/parser', |
|||
'<template>': 'espree' |
|||
}, |
|||
ecmaVersion: 2022, |
|||
sourceType: 'module', |
|||
extraFileExtensions: ['.vue'], |
|||
ecmaFeatures: { |
|||
jsx: true |
|||
} |
|||
}, |
|||
plugins: [ |
|||
'@typescript-eslint', |
|||
'vue' |
|||
], |
|||
rules: { |
|||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', |
|||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', |
|||
'@typescript-eslint/no-explicit-any': 'off', |
|||
'vue/multi-word-component-names': 'off', |
|||
'@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }] |
|||
}, |
|||
overrides: [ |
|||
{ |
|||
files: ['*.ts', '*.tsx', '*.mts'], |
|||
parser: '@typescript-eslint/parser' |
|||
}, |
|||
{ |
|||
files: ['*.js', '*.jsx', '*.mjs'], |
|||
parser: '@typescript-eslint/parser' |
|||
} |
|||
], |
|||
settings: { |
|||
'import/resolver': { |
|||
node: { |
|||
extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue', '.mjs', '.mts'] |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
@ -0,0 +1,124 @@ |
|||
import { |
|||
BarcodeScanner, |
|||
BarcodeFormat, |
|||
LensFacing, |
|||
ScanResult |
|||
} from '@capacitor-mlkit/barcode-scanning' |
|||
import type { QRScannerService, ScanListener } from './types' |
|||
import { logger } from '../../utils/logger' |
|||
|
|||
export class CapacitorQRScanner implements QRScannerService { |
|||
private scanListener: ScanListener | null = null |
|||
private isScanning = false |
|||
private listenerHandles: Array<() => Promise<void>> = [] |
|||
|
|||
async checkPermissions() { |
|||
try { |
|||
const { camera } = await BarcodeScanner.checkPermissions() |
|||
return camera === 'granted' |
|||
} catch (error) { |
|||
logger.error('Error checking camera permissions:', error) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
async requestPermissions() { |
|||
try { |
|||
const { camera } = await BarcodeScanner.requestPermissions() |
|||
return camera === 'granted' |
|||
} catch (error) { |
|||
logger.error('Error requesting camera permissions:', error) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
async isSupported() { |
|||
try { |
|||
const { supported } = await BarcodeScanner.isSupported() |
|||
return supported |
|||
} catch (error) { |
|||
logger.error('Error checking barcode scanner support:', error) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
async startScan() { |
|||
if (this.isScanning) { |
|||
logger.warn('Scanner is already active') |
|||
return |
|||
} |
|||
|
|||
try { |
|||
// First register listeners before starting scan
|
|||
await this.registerListeners() |
|||
|
|||
this.isScanning = true |
|||
await BarcodeScanner.startScan({ |
|||
formats: [BarcodeFormat.QrCode], |
|||
lensFacing: LensFacing.Back |
|||
}) |
|||
} catch (error) { |
|||
// Ensure cleanup on error
|
|||
this.isScanning = false |
|||
await this.removeListeners() |
|||
logger.error('Error starting barcode scan:', error) |
|||
throw error |
|||
} |
|||
} |
|||
|
|||
private async registerListeners() { |
|||
try { |
|||
const handle = await BarcodeScanner.addListener( |
|||
'barcodesScanned', |
|||
async (result: ScanResult) => { |
|||
if (result.barcodes.length > 0 && this.scanListener) { |
|||
const barcode = result.barcodes[0] |
|||
this.scanListener.onScan(barcode.rawValue) |
|||
await this.stopScan() |
|||
} |
|||
} |
|||
) |
|||
this.listenerHandles.push(() => handle.remove()) |
|||
} catch (error) { |
|||
logger.error('Error registering barcode listener:', error) |
|||
throw error |
|||
} |
|||
} |
|||
|
|||
private async removeListeners() { |
|||
for (const remove of this.listenerHandles) { |
|||
try { |
|||
await remove() |
|||
} catch (error) { |
|||
logger.error('Error removing listener:', error) |
|||
} |
|||
} |
|||
this.listenerHandles = [] |
|||
} |
|||
|
|||
async stopScan() { |
|||
if (!this.isScanning) { |
|||
return |
|||
} |
|||
|
|||
try { |
|||
// First stop the scan
|
|||
await BarcodeScanner.stopScan() |
|||
} catch (error) { |
|||
logger.error('Error stopping barcode scan:', error) |
|||
} finally { |
|||
// Always cleanup state even if stop fails
|
|||
this.isScanning = false |
|||
await this.removeListeners() |
|||
} |
|||
} |
|||
|
|||
addListener(listener: ScanListener): void { |
|||
this.scanListener = listener |
|||
} |
|||
|
|||
async cleanup(): Promise<void> { |
|||
await this.stopScan() |
|||
this.scanListener = null |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
import type { QRScannerService, ScanListener } from './types' |
|||
import QRScannerDialog from './QRScannerDialog.vue' |
|||
import { createApp, type App } from 'vue' |
|||
import { logger } from '../../utils/logger' |
|||
|
|||
// Import platform-specific flags from Vite config
|
|||
declare const __USE_QR_READER__: boolean |
|||
|
|||
export class WebDialogQRScanner implements QRScannerService { |
|||
private dialogApp: App | null = null |
|||
private dialogElement: HTMLDivElement | null = null |
|||
private scanListener: ScanListener | null = null |
|||
|
|||
async checkPermissions(): Promise<boolean> { |
|||
return navigator?.mediaDevices !== undefined |
|||
} |
|||
|
|||
async requestPermissions(): Promise<boolean> { |
|||
try { |
|||
const stream = await navigator.mediaDevices.getUserMedia({ video: true }) |
|||
stream.getTracks().forEach((track) => track.stop()) |
|||
return true |
|||
} catch (error) { |
|||
logger.error('Failed to get camera permissions:', error) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
async isSupported(): Promise<boolean> { |
|||
return Promise.resolve( |
|||
__USE_QR_READER__ && navigator?.mediaDevices !== undefined |
|||
) |
|||
} |
|||
|
|||
async startScan(): Promise<void> { |
|||
if (!(await this.isSupported())) { |
|||
throw new Error('QR scanning is not supported in this environment') |
|||
} |
|||
|
|||
this.dialogElement = document.createElement('div') |
|||
document.body.appendChild(this.dialogElement) |
|||
|
|||
this.dialogApp = createApp(QRScannerDialog, { |
|||
onScan: (result: string) => { |
|||
if (this.scanListener) { |
|||
this.scanListener.onScan(result) |
|||
} |
|||
}, |
|||
onClose: () => { |
|||
this.stopScan() |
|||
} |
|||
}) |
|||
|
|||
this.dialogApp.mount(this.dialogElement) |
|||
} |
|||
|
|||
async stopScan(): Promise<void> { |
|||
if (this.dialogApp && this.dialogElement) { |
|||
this.dialogApp.unmount() |
|||
this.dialogElement.remove() |
|||
this.dialogApp = null |
|||
this.dialogElement = null |
|||
} |
|||
} |
|||
|
|||
addListener(listener: ScanListener): void { |
|||
this.scanListener = listener |
|||
} |
|||
|
|||
async cleanup(): Promise<void> { |
|||
await this.stopScan() |
|||
this.scanListener = null |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
import { Capacitor } from '@capacitor/core' |
|||
import type { QRScannerService } from './types' |
|||
import { logger } from '../../utils/logger' |
|||
import { WebDialogQRScanner } from './WebDialogScanner' |
|||
import { CapacitorQRScanner } from './CapacitorScanner' |
|||
|
|||
// Import platform-specific flags from Vite config
|
|||
declare const __USE_QR_READER__: boolean |
|||
declare const __IS_MOBILE__: boolean |
|||
|
|||
export class QRScannerFactory { |
|||
private static instance: QRScannerService | null = null |
|||
|
|||
static getInstance(): QRScannerService { |
|||
if (!this.instance) { |
|||
// Use platform-specific flags for more accurate detection
|
|||
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) { |
|||
logger.log('Creating native QR scanner instance') |
|||
this.instance = new CapacitorQRScanner() |
|||
} else if (__USE_QR_READER__) { |
|||
logger.log('Creating web QR scanner instance') |
|||
this.instance = new WebDialogQRScanner() |
|||
} else { |
|||
throw new Error( |
|||
'No QR scanner implementation available for this platform' |
|||
) |
|||
} |
|||
} |
|||
return this.instance |
|||
} |
|||
|
|||
static async cleanup() { |
|||
if (this.instance) { |
|||
await this.instance.cleanup() |
|||
this.instance = null |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
export interface ScanListener { |
|||
onScan: (result: string) => void |
|||
onError?: (error: Error) => void |
|||
} |
|||
|
|||
export interface QRScannerService { |
|||
checkPermissions(): Promise<boolean> |
|||
requestPermissions(): Promise<boolean> |
|||
isSupported(): Promise<boolean> |
|||
startScan(): Promise<void> |
|||
stopScan(): Promise<void> |
|||
addListener(listener: ScanListener): void |
|||
cleanup(): Promise<void> |
|||
} |
|||
@ -1,19 +1,19 @@ |
|||
import { PerspectiveCamera } from "three"; |
|||
import { PerspectiveCamera } from 'three' |
|||
|
|||
function createCamera() { |
|||
const camera = new PerspectiveCamera( |
|||
35, // fov = Field Of View
|
|||
1, // aspect ratio (dummy value)
|
|||
0.1, // near clipping plane
|
|||
350, // far clipping plane
|
|||
); |
|||
350 // far clipping plane
|
|||
) |
|||
|
|||
// move the camera back so we can view the scene
|
|||
camera.position.set(0, 100, 200); |
|||
camera.position.set(0, 100, 200) |
|||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|||
camera.tick = () => {}; |
|||
camera.tick = () => {} |
|||
|
|||
return camera; |
|||
return camera |
|||
} |
|||
|
|||
export { createCamera }; |
|||
export { createCamera } |
|||
|
|||
@ -1,14 +1,14 @@ |
|||
import { DirectionalLight, DirectionalLightHelper } from "three"; |
|||
import { DirectionalLight, DirectionalLightHelper } from 'three' |
|||
|
|||
function createLights(color) { |
|||
const light = new DirectionalLight(color, 4); |
|||
const lightHelper = new DirectionalLightHelper(light, 0); |
|||
light.position.set(60, 100, 30); |
|||
const light = new DirectionalLight(color, 4) |
|||
const lightHelper = new DirectionalLightHelper(light, 0) |
|||
light.position.set(60, 100, 30) |
|||
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|||
light.tick = () => {}; |
|||
light.tick = () => {} |
|||
|
|||
return { light, lightHelper }; |
|||
return { light, lightHelper } |
|||
} |
|||
|
|||
export { createLights }; |
|||
export { createLights } |
|||
|
|||
@ -1,29 +1,29 @@ |
|||
import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from "three"; |
|||
import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from 'three' |
|||
|
|||
export function createTerrain(props) { |
|||
const loader = new TextureLoader(); |
|||
const height = loader.load("img/textures/leafy-autumn-forest-floor.jpg"); |
|||
const loader = new TextureLoader() |
|||
const height = loader.load('img/textures/leafy-autumn-forest-floor.jpg') |
|||
// w h
|
|||
const geometry = new PlaneGeometry(props.width, props.height, 64, 64); |
|||
const geometry = new PlaneGeometry(props.width, props.height, 64, 64) |
|||
|
|||
const material = new MeshLambertMaterial({ |
|||
color: props.color, |
|||
flatShading: true, |
|||
map: height, |
|||
map: height |
|||
//displacementMap: height,
|
|||
//displacementScale: 5,
|
|||
}); |
|||
}) |
|||
|
|||
const plane = new Mesh(geometry, material); |
|||
plane.position.set(0, 0, 0); |
|||
plane.rotation.x -= Math.PI * 0.5; |
|||
const plane = new Mesh(geometry, material) |
|||
plane.position.set(0, 0, 0) |
|||
plane.rotation.x -= Math.PI * 0.5 |
|||
|
|||
//Storing our original vertices position on a new attribute
|
|||
plane.geometry.attributes.position.originalPosition = |
|||
plane.geometry.attributes.position.array; |
|||
plane.geometry.attributes.position.array |
|||
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|||
plane.tick = () => {}; |
|||
plane.tick = () => {} |
|||
|
|||
return plane; |
|||
return plane |
|||
} |
|||
|
|||
@ -1,11 +1,11 @@ |
|||
import { Color, Scene } from "three"; |
|||
import { Color, Scene } from 'three' |
|||
|
|||
function createScene(color) { |
|||
const scene = new Scene(); |
|||
const scene = new Scene() |
|||
|
|||
scene.background = new Color(color); |
|||
scene.background = new Color(color) |
|||
//scene.fog = new Fog(color, 60, 90);
|
|||
return scene; |
|||
return scene |
|||
} |
|||
|
|||
export { createScene }; |
|||
export { createScene } |
|||
|
|||
@ -1,33 +1,33 @@ |
|||
import { Clock } from "three"; |
|||
import { Clock } from 'three' |
|||
|
|||
const clock = new Clock(); |
|||
const clock = new Clock() |
|||
|
|||
class Loop { |
|||
constructor(camera, scene, renderer) { |
|||
this.camera = camera; |
|||
this.scene = scene; |
|||
this.renderer = renderer; |
|||
this.updatables = []; |
|||
this.camera = camera |
|||
this.scene = scene |
|||
this.renderer = renderer |
|||
this.updatables = [] |
|||
} |
|||
|
|||
start() { |
|||
this.renderer.setAnimationLoop(() => { |
|||
this.tick(); |
|||
this.tick() |
|||
// render a frame
|
|||
this.renderer.render(this.scene, this.camera); |
|||
}); |
|||
this.renderer.render(this.scene, this.camera) |
|||
}) |
|||
} |
|||
|
|||
stop() { |
|||
this.renderer.setAnimationLoop(null); |
|||
this.renderer.setAnimationLoop(null) |
|||
} |
|||
|
|||
tick() { |
|||
const delta = clock.getDelta(); |
|||
const delta = clock.getDelta() |
|||
for (const object of this.updatables) { |
|||
object.tick(delta); |
|||
object.tick(delta) |
|||
} |
|||
} |
|||
} |
|||
|
|||
export { Loop }; |
|||
export { Loop } |
|||
|
|||
@ -1,33 +1,33 @@ |
|||
const setSize = (container, camera, renderer) => { |
|||
// These are great for full-screen, which adjusts to a window.
|
|||
const height = window.innerHeight; |
|||
const width = window.innerWidth - 50; |
|||
const height = window.innerHeight |
|||
const width = window.innerWidth - 50 |
|||
// These are better for fitting in a container, which stays that size.
|
|||
//const height = container.scrollHeight;
|
|||
//const width = container.scrollWidth;
|
|||
|
|||
camera.aspect = width / height; |
|||
camera.updateProjectionMatrix(); |
|||
camera.aspect = width / height |
|||
camera.updateProjectionMatrix() |
|||
|
|||
renderer.setSize(width, height); |
|||
renderer.setPixelRatio(window.devicePixelRatio); |
|||
}; |
|||
renderer.setSize(width, height) |
|||
renderer.setPixelRatio(window.devicePixelRatio) |
|||
} |
|||
|
|||
class Resizer { |
|||
constructor(container, camera, renderer) { |
|||
// set initial size on load
|
|||
setSize(container, camera, renderer); |
|||
setSize(container, camera, renderer) |
|||
|
|||
window.addEventListener("resize", () => { |
|||
window.addEventListener('resize', () => { |
|||
// set the size again if a resize occurs
|
|||
setSize(container, camera, renderer); |
|||
setSize(container, camera, renderer) |
|||
// perform any custom actions
|
|||
this.onResize(); |
|||
}); |
|||
this.onResize() |
|||
}) |
|||
} |
|||
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|||
onResize() {} |
|||
} |
|||
|
|||
export { Resizer }; |
|||
export { Resizer } |
|||
|
|||
@ -1,13 +1,13 @@ |
|||
import { WebGLRenderer } from "three"; |
|||
import { WebGLRenderer } from 'three' |
|||
|
|||
function createRenderer() { |
|||
const renderer = new WebGLRenderer({ antialias: true }); |
|||
const renderer = new WebGLRenderer({ antialias: true }) |
|||
|
|||
// turn on the physically correct lighting model
|
|||
// (The browser complains: "THREE.WebGLRenderer: the property .physicallyCorrectLights has been removed. Set renderer.useLegacyLights instead." However, that changes the lighting in a way that doesn't look better.)
|
|||
renderer.physicallyCorrectLights = true; |
|||
renderer.physicallyCorrectLights = true |
|||
|
|||
return renderer; |
|||
return renderer |
|||
} |
|||
|
|||
export { createRenderer }; |
|||
export { createRenderer } |
|||
|
|||
@ -1,24 +1,24 @@ |
|||
export interface ContactMethod { |
|||
label: string; |
|||
type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
|
|||
value: string; |
|||
label: string |
|||
type: string // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
|
|||
value: string |
|||
} |
|||
|
|||
export interface Contact { |
|||
//
|
|||
// When adding a property, consider whether it should be added when exporting & sharing contacts.
|
|||
|
|||
did: string; |
|||
contactMethods?: Array<ContactMethod>; |
|||
name?: string; |
|||
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
|||
notes?: string; |
|||
profileImageUrl?: string; |
|||
publicKeyBase64?: string; |
|||
seesMe?: boolean; // cached value of the server setting
|
|||
registered?: boolean; // cached value of the server setting
|
|||
did: string |
|||
contactMethods?: Array<ContactMethod> |
|||
name?: string |
|||
nextPubKeyHashB64?: string // base64-encoded SHA256 hash of next public key
|
|||
notes?: string |
|||
profileImageUrl?: string |
|||
publicKeyBase64?: string |
|||
seesMe?: boolean // cached value of the server setting
|
|||
registered?: boolean // cached value of the server setting
|
|||
} |
|||
|
|||
export const ContactSchema = { |
|||
contacts: "&did, name", // no need to key by other things
|
|||
}; |
|||
contacts: '&did, name' // no need to key by other things
|
|||
} |
|||
|
|||
@ -1,11 +1,11 @@ |
|||
export interface Log { |
|||
date: string; |
|||
message: string; |
|||
date: string |
|||
message: string |
|||
} |
|||
|
|||
export const LogSchema = { |
|||
// Currently keyed by "date" because A) today's log data is what we need so we append, and
|
|||
// B) we don't want it to grow so we remove everything if this is the first entry today.
|
|||
// See safari-notifications.js logMessage for the associated logic.
|
|||
logs: "date", // definitely don't key by the potentially large message field
|
|||
}; |
|||
logs: 'date' // definitely don't key by the potentially large message field
|
|||
} |
|||
|
|||
@ -1,12 +1,12 @@ |
|||
// for ephemeral uses, eg. passing a blob from the service worker to the main thread
|
|||
|
|||
export type Temp = { |
|||
id: string; |
|||
blob?: Blob; // deprecated because webkit (Safari) does not support Blob
|
|||
blobB64?: string; // base64-encoded blob
|
|||
}; |
|||
id: string |
|||
blob?: Blob // deprecated because webkit (Safari) does not support Blob
|
|||
blobB64?: string // base64-encoded blob
|
|||
} |
|||
|
|||
/** |
|||
* Schema for the Temp table in the database. |
|||
*/ |
|||
export const TempSchema = { temp: "id" }; |
|||
export const TempSchema = { temp: 'id' } |
|||
|
|||
@ -1,78 +1,78 @@ |
|||
const { contextBridge, ipcRenderer } = require("electron"); |
|||
const { contextBridge, ipcRenderer } = require('electron') |
|||
|
|||
const logger = { |
|||
log: (message, ...args) => { |
|||
if (process.env.NODE_ENV !== "production") { |
|||
if (process.env.NODE_ENV !== 'production') { |
|||
/* eslint-disable no-console */ |
|||
console.log(message, ...args); |
|||
console.log(message, ...args) |
|||
/* eslint-enable no-console */ |
|||
} |
|||
}, |
|||
warn: (message, ...args) => { |
|||
if (process.env.NODE_ENV !== "production") { |
|||
if (process.env.NODE_ENV !== 'production') { |
|||
/* eslint-disable no-console */ |
|||
console.warn(message, ...args); |
|||
console.warn(message, ...args) |
|||
/* eslint-enable no-console */ |
|||
} |
|||
}, |
|||
error: (message, ...args) => { |
|||
/* eslint-disable no-console */ |
|||
console.error(message, ...args); // Errors should always be logged
|
|||
console.error(message, ...args) // Errors should always be logged
|
|||
/* eslint-enable no-console */ |
|||
}, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
// Use a more direct path resolution approach
|
|||
const getPath = (pathType) => { |
|||
switch (pathType) { |
|||
case "userData": |
|||
case 'userData': |
|||
return ( |
|||
process.env.APPDATA || |
|||
(process.platform === "darwin" |
|||
(process.platform === 'darwin' |
|||
? `${process.env.HOME}/Library/Application Support` |
|||
: `${process.env.HOME}/.local/share`) |
|||
); |
|||
case "home": |
|||
return process.env.HOME; |
|||
case "appPath": |
|||
return process.resourcesPath; |
|||
) |
|||
case 'home': |
|||
return process.env.HOME |
|||
case 'appPath': |
|||
return process.resourcesPath |
|||
default: |
|||
return ""; |
|||
return '' |
|||
} |
|||
}; |
|||
} |
|||
|
|||
logger.log("Preload script starting..."); |
|||
logger.log('Preload script starting...') |
|||
|
|||
try { |
|||
contextBridge.exposeInMainWorld("electronAPI", { |
|||
contextBridge.exposeInMainWorld('electronAPI', { |
|||
// Path utilities
|
|||
getPath, |
|||
|
|||
// IPC functions
|
|||
send: (channel, data) => { |
|||
const validChannels = ["toMain"]; |
|||
const validChannels = ['toMain'] |
|||
if (validChannels.includes(channel)) { |
|||
ipcRenderer.send(channel, data); |
|||
ipcRenderer.send(channel, data) |
|||
} |
|||
}, |
|||
receive: (channel, func) => { |
|||
const validChannels = ["fromMain"]; |
|||
const validChannels = ['fromMain'] |
|||
if (validChannels.includes(channel)) { |
|||
ipcRenderer.on(channel, (event, ...args) => func(...args)); |
|||
ipcRenderer.on(channel, (event, ...args) => func(...args)) |
|||
} |
|||
}, |
|||
// Environment info
|
|||
env: { |
|||
isElectron: true, |
|||
isDev: process.env.NODE_ENV === "development", |
|||
isDev: process.env.NODE_ENV === 'development' |
|||
}, |
|||
// Path utilities
|
|||
getBasePath: () => { |
|||
return process.env.NODE_ENV === "development" ? "/" : "./"; |
|||
}, |
|||
}); |
|||
return process.env.NODE_ENV === 'development' ? '/' : './' |
|||
} |
|||
}) |
|||
|
|||
logger.log("Preload script completed successfully"); |
|||
logger.log('Preload script completed successfully') |
|||
} catch (error) { |
|||
logger.error("Error in preload script:", error); |
|||
logger.error('Error in preload script:', error) |
|||
} |
|||
|
|||
@ -1,58 +1,58 @@ |
|||
import { AxiosResponse } from "axios"; |
|||
import { GiverReceiverInputInfo } from "../libs/util"; |
|||
import { ErrorResult, ResultWithType } from "./common"; |
|||
import { AxiosResponse } from 'axios' |
|||
import { GiverReceiverInputInfo } from '../libs/util' |
|||
import { ErrorResult, ResultWithType } from './common' |
|||
|
|||
export interface GiverOutputInfo { |
|||
action: string; |
|||
giver?: GiverReceiverInputInfo; |
|||
description?: string; |
|||
amount?: number; |
|||
unitCode?: string; |
|||
action: string |
|||
giver?: GiverReceiverInputInfo |
|||
description?: string |
|||
amount?: number |
|||
unitCode?: string |
|||
} |
|||
|
|||
export interface ClaimResult { |
|||
success: { claimId: string; handleId: string }; |
|||
error: { code: string; message: string }; |
|||
success: { claimId: string; handleId: string } |
|||
error: { code: string; message: string } |
|||
} |
|||
|
|||
export interface VerifiableCredential { |
|||
exp?: number; |
|||
iat: number; |
|||
iss: string; |
|||
exp?: number |
|||
iat: number |
|||
iss: string |
|||
vc: { |
|||
"@context": string[]; |
|||
type: string[]; |
|||
credentialSubject: VerifiableCredentialSubject; |
|||
}; |
|||
'@context': string[] |
|||
type: string[] |
|||
credentialSubject: VerifiableCredentialSubject |
|||
} |
|||
} |
|||
|
|||
export interface VerifiableCredentialSubject { |
|||
"@context": string; |
|||
"@type": string; |
|||
[key: string]: unknown; |
|||
'@context': string |
|||
'@type': string |
|||
[key: string]: unknown |
|||
} |
|||
|
|||
export interface WorldProperties { |
|||
startTime?: string; |
|||
endTime?: string; |
|||
startTime?: string |
|||
endTime?: string |
|||
} |
|||
|
|||
export interface ProviderInfo { |
|||
/** |
|||
* Could be a DID or a handleId that identifies the provider |
|||
*/ |
|||
identifier: string; |
|||
identifier: string |
|||
/** |
|||
* Indicates if the provider link has been confirmed |
|||
*/ |
|||
linkConfirmed: boolean; |
|||
linkConfirmed: boolean |
|||
} |
|||
|
|||
// Type for createAndSubmitClaim result
|
|||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult; |
|||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult |
|||
|
|||
// Update SuccessResult to use ClaimResult
|
|||
export interface SuccessResult extends ResultWithType { |
|||
type: "success"; |
|||
response: AxiosResponse<ClaimResult>; |
|||
type: 'success' |
|||
response: AxiosResponse<ClaimResult> |
|||
} |
|||
|
|||
@ -1,68 +1,68 @@ |
|||
import { GenericVerifiableCredential } from "./common"; |
|||
import { GenericVerifiableCredential } from './common' |
|||
|
|||
export interface AgreeVerifiableCredential { |
|||
"@context": string; |
|||
"@type": string; |
|||
object: Record<string, unknown>; |
|||
'@context': string |
|||
'@type': string |
|||
object: Record<string, unknown> |
|||
} |
|||
|
|||
// Note that previous VCs may have additional fields.
|
|||
// https://endorser.ch/doc/html/transactions.html#id4
|
|||
export interface GiveVerifiableCredential extends GenericVerifiableCredential { |
|||
"@context"?: string; |
|||
"@type": "GiveAction"; |
|||
agent?: { identifier: string }; |
|||
description?: string; |
|||
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[]; |
|||
identifier?: string; |
|||
image?: string; |
|||
object?: { amountOfThisGood: number; unitCode: string }; |
|||
provider?: GenericVerifiableCredential; |
|||
recipient?: { identifier: string }; |
|||
'@context'?: string |
|||
'@type': 'GiveAction' |
|||
agent?: { identifier: string } |
|||
description?: string |
|||
fulfills?: { '@type': string; identifier?: string; lastClaimId?: string }[] |
|||
identifier?: string |
|||
image?: string |
|||
object?: { amountOfThisGood: number; unitCode: string } |
|||
provider?: GenericVerifiableCredential |
|||
recipient?: { identifier: string } |
|||
} |
|||
|
|||
// Note that previous VCs may have additional fields.
|
|||
// https://endorser.ch/doc/html/transactions.html#id8
|
|||
export interface OfferVerifiableCredential extends GenericVerifiableCredential { |
|||
"@context"?: string; |
|||
"@type": "Offer"; |
|||
description?: string; |
|||
includesObject?: { amountOfThisGood: number; unitCode: string }; |
|||
'@context'?: string |
|||
'@type': 'Offer' |
|||
description?: string |
|||
includesObject?: { amountOfThisGood: number; unitCode: string } |
|||
itemOffered?: { |
|||
description?: string; |
|||
description?: string |
|||
isPartOf?: { |
|||
identifier?: string; |
|||
lastClaimId?: string; |
|||
"@type"?: string; |
|||
name?: string; |
|||
}; |
|||
}; |
|||
offeredBy?: { identifier: string }; |
|||
recipient?: { identifier: string }; |
|||
validThrough?: string; |
|||
identifier?: string |
|||
lastClaimId?: string |
|||
'@type'?: string |
|||
name?: string |
|||
} |
|||
} |
|||
offeredBy?: { identifier: string } |
|||
recipient?: { identifier: string } |
|||
validThrough?: string |
|||
} |
|||
|
|||
// Note that previous VCs may have additional fields.
|
|||
// https://endorser.ch/doc/html/transactions.html#id7
|
|||
export interface PlanVerifiableCredential extends GenericVerifiableCredential { |
|||
"@context": "https://schema.org"; |
|||
"@type": "PlanAction"; |
|||
name: string; |
|||
agent?: { identifier: string }; |
|||
description?: string; |
|||
identifier?: string; |
|||
lastClaimId?: string; |
|||
'@context': 'https://schema.org' |
|||
'@type': 'PlanAction' |
|||
name: string |
|||
agent?: { identifier: string } |
|||
description?: string |
|||
identifier?: string |
|||
lastClaimId?: string |
|||
location?: { |
|||
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number }; |
|||
}; |
|||
geo: { '@type': 'GeoCoordinates'; latitude: number; longitude: number } |
|||
} |
|||
} |
|||
|
|||
// AKA Registration & RegisterAction
|
|||
export interface RegisterVerifiableCredential { |
|||
"@context": string; |
|||
"@type": "RegisterAction"; |
|||
agent: { identifier: string }; |
|||
identifier?: string; |
|||
object: string; |
|||
participant?: { identifier: string }; |
|||
'@context': string |
|||
'@type': 'RegisterAction' |
|||
agent: { identifier: string } |
|||
identifier?: string |
|||
object: string |
|||
participant?: { identifier: string } |
|||
} |
|||
|
|||
@ -1,36 +1,36 @@ |
|||
// similar to VerifiableCredentialSubject... maybe rename this
|
|||
export interface GenericVerifiableCredential { |
|||
"@context"?: string; |
|||
"@type": string; |
|||
[key: string]: unknown; |
|||
'@context'?: string |
|||
'@type': string |
|||
[key: string]: unknown |
|||
} |
|||
|
|||
export interface GenericCredWrapper<T extends GenericVerifiableCredential> { |
|||
claim: T; |
|||
claimType?: string; |
|||
handleId: string; |
|||
id: string; |
|||
issuedAt: string; |
|||
issuer: string; |
|||
publicUrls?: Record<string, string>; |
|||
claim: T |
|||
claimType?: string |
|||
handleId: string |
|||
id: string |
|||
issuedAt: string |
|||
issuer: string |
|||
publicUrls?: Record<string, string> |
|||
} |
|||
|
|||
export interface ResultWithType { |
|||
type: string; |
|||
type: string |
|||
} |
|||
|
|||
export interface ErrorResponse { |
|||
error?: { |
|||
message?: string; |
|||
}; |
|||
message?: string |
|||
} |
|||
} |
|||
|
|||
export interface InternalError { |
|||
error: string; |
|||
userMessage?: string; |
|||
error: string |
|||
userMessage?: string |
|||
} |
|||
|
|||
export interface ErrorResult extends ResultWithType { |
|||
type: "error"; |
|||
error: InternalError; |
|||
type: 'error' |
|||
error: InternalError |
|||
} |
|||
|
|||
@ -1,7 +1,7 @@ |
|||
export * from "./claims"; |
|||
export * from "./claims-result"; |
|||
export * from "./common"; |
|||
export * from "./limits"; |
|||
export * from "./records"; |
|||
export * from "./user"; |
|||
export * from "./deepLinks"; |
|||
export * from './claims' |
|||
export * from './claims-result' |
|||
export * from './common' |
|||
export * from './limits' |
|||
export * from './records' |
|||
export * from './user' |
|||
export * from './deepLinks' |
|||
|
|||
@ -1,14 +1,14 @@ |
|||
export interface EndorserRateLimits { |
|||
doneClaimsThisWeek: string; |
|||
doneRegistrationsThisMonth: string; |
|||
maxClaimsPerWeek: string; |
|||
maxRegistrationsPerMonth: string; |
|||
nextMonthBeginDateTime: string; |
|||
nextWeekBeginDateTime: string; |
|||
doneClaimsThisWeek: string |
|||
doneRegistrationsThisMonth: string |
|||
maxClaimsPerWeek: string |
|||
maxRegistrationsPerMonth: string |
|||
nextMonthBeginDateTime: string |
|||
nextWeekBeginDateTime: string |
|||
} |
|||
|
|||
export interface ImageRateLimits { |
|||
doneImagesThisWeek: string; |
|||
maxImagesPerWeek: string; |
|||
nextWeekBeginDateTime: string; |
|||
doneImagesThisWeek: string |
|||
maxImagesPerWeek: string |
|||
nextWeekBeginDateTime: string |
|||
} |
|||
|
|||
@ -1,8 +1,8 @@ |
|||
export interface UserInfo { |
|||
did: string; |
|||
name: string; |
|||
publicEncKey: string; |
|||
registered: boolean; |
|||
profileImageUrl?: string; |
|||
nextPublicEncKeyHash?: string; |
|||
did: string |
|||
name: string |
|||
publicEncKey: string |
|||
registered: boolean |
|||
profileImageUrl?: string |
|||
nextPublicEncKeyHash?: string |
|||
} |
|||
|
|||
@ -1,11 +1,11 @@ |
|||
export function urlBase64ToUint8Array(base64String: string): Uint8Array { |
|||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4); |
|||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); |
|||
const rawData = window.atob(base64); |
|||
const outputArray = new Uint8Array(rawData.length); |
|||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4) |
|||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/') |
|||
const rawData = window.atob(base64) |
|||
const outputArray = new Uint8Array(rawData.length) |
|||
|
|||
for (let i = 0; i < rawData.length; ++i) { |
|||
outputArray[i] = rawData.charCodeAt(i); |
|||
outputArray[i] = rawData.charCodeAt(i) |
|||
} |
|||
return outputArray; |
|||
return outputArray |
|||
} |
|||
|
|||
File diff suppressed because it is too large
@ -1,9 +1,9 @@ |
|||
export interface UserProfile { |
|||
description: string; |
|||
locLat?: number; |
|||
locLon?: number; |
|||
locLat2?: number; |
|||
locLon2?: number; |
|||
issuerDid: string; |
|||
rowId?: string; // set on profile retrieved from server
|
|||
description: string |
|||
locLat?: number |
|||
locLon?: number |
|||
locLat2?: number |
|||
locLon2?: number |
|||
issuerDid: string |
|||
rowId?: string // set on profile retrieved from server
|
|||
} |
|||
|
|||
@ -1,7 +1,7 @@ |
|||
// see also ../constants/app.ts and
|
|||
|
|||
function didProviderName(netName: string) { |
|||
return "did:ethr" + (netName === "mainnet" ? "" : ":" + netName); |
|||
return 'did:ethr' + (netName === 'mainnet' ? '' : ':' + netName) |
|||
} |
|||
|
|||
export const DEFAULT_DID_PROVIDER_NAME = didProviderName("mainnet"); |
|||
export const DEFAULT_DID_PROVIDER_NAME = didProviderName('mainnet') |
|||
|
|||
@ -1,61 +1,61 @@ |
|||
import { createPinia } from "pinia"; |
|||
import { App as VueApp, ComponentPublicInstance, createApp } from "vue"; |
|||
import App from "./App.vue"; |
|||
import router from "./router"; |
|||
import axios from "axios"; |
|||
import VueAxios from "vue-axios"; |
|||
import Notifications from "notiwind"; |
|||
import "./assets/styles/tailwind.css"; |
|||
import { FontAwesomeIcon } from "./lib/fontawesome"; |
|||
import Camera from "simple-vue-camera"; |
|||
import { logger } from "./utils/logger"; |
|||
import { createPinia } from 'pinia' |
|||
import { App as VueApp, ComponentPublicInstance, createApp } from 'vue' |
|||
import App from './App.vue' |
|||
import router from './router' |
|||
import axios from 'axios' |
|||
import VueAxios from 'vue-axios' |
|||
import Notifications from 'notiwind' |
|||
import './assets/styles/tailwind.css' |
|||
import { FontAwesomeIcon } from './lib/fontawesome' |
|||
import Camera from 'simple-vue-camera' |
|||
import { logger } from './utils/logger' |
|||
|
|||
// Global Error Handler
|
|||
function setupGlobalErrorHandler(app: VueApp) { |
|||
logger.log("[App Init] Setting up global error handler"); |
|||
logger.log('[App Init] Setting up global error handler') |
|||
app.config.errorHandler = ( |
|||
err: unknown, |
|||
instance: ComponentPublicInstance | null, |
|||
info: string, |
|||
info: string |
|||
) => { |
|||
logger.error("[App Error] Global Error Handler:", { |
|||
logger.error('[App Error] Global Error Handler:', { |
|||
error: err, |
|||
info, |
|||
component: instance?.$options.name || "unknown", |
|||
}); |
|||
component: instance?.$options.name || 'unknown' |
|||
}) |
|||
alert( |
|||
(err instanceof Error ? err.message : "Something bad happened") + |
|||
" - Try reloading or restarting the app.", |
|||
); |
|||
}; |
|||
(err instanceof Error ? err.message : 'Something bad happened') + |
|||
' - Try reloading or restarting the app.' |
|||
) |
|||
} |
|||
} |
|||
|
|||
// Function to initialize the app
|
|||
export function initializeApp() { |
|||
logger.log("[App Init] Starting app initialization"); |
|||
logger.log("[App Init] Platform:", process.env.VITE_PLATFORM); |
|||
logger.log('[App Init] Starting app initialization') |
|||
logger.log('[App Init] Platform:', process.env.VITE_PLATFORM) |
|||
|
|||
const app = createApp(App); |
|||
logger.log("[App Init] Vue app created"); |
|||
const app = createApp(App) |
|||
logger.log('[App Init] Vue app created') |
|||
|
|||
app.component("FontAwesome", FontAwesomeIcon).component("camera", Camera); |
|||
logger.log("[App Init] Components registered"); |
|||
app.component('FontAwesome', FontAwesomeIcon).component('camera', Camera) |
|||
logger.log('[App Init] Components registered') |
|||
|
|||
const pinia = createPinia(); |
|||
app.use(pinia); |
|||
logger.log("[App Init] Pinia store initialized"); |
|||
const pinia = createPinia() |
|||
app.use(pinia) |
|||
logger.log('[App Init] Pinia store initialized') |
|||
|
|||
app.use(VueAxios, axios); |
|||
logger.log("[App Init] Axios initialized"); |
|||
app.use(VueAxios, axios) |
|||
logger.log('[App Init] Axios initialized') |
|||
|
|||
app.use(router); |
|||
logger.log("[App Init] Router initialized"); |
|||
app.use(router) |
|||
logger.log('[App Init] Router initialized') |
|||
|
|||
app.use(Notifications); |
|||
logger.log("[App Init] Notifications initialized"); |
|||
app.use(Notifications) |
|||
logger.log('[App Init] Notifications initialized') |
|||
|
|||
setupGlobalErrorHandler(app); |
|||
logger.log("[App Init] App initialization complete"); |
|||
setupGlobalErrorHandler(app) |
|||
logger.log('[App Init] App initialization complete') |
|||
|
|||
return app; |
|||
return app |
|||
} |
|||
|
|||
@ -1,4 +1,4 @@ |
|||
import { initializeApp } from "./main.common"; |
|||
import { initializeApp } from './main.common' |
|||
|
|||
const app = initializeApp(); |
|||
app.mount("#app"); |
|||
const app = initializeApp() |
|||
app.mount('#app') |
|||
|
|||
@ -1,4 +1,4 @@ |
|||
import { initializeApp } from "./main.common"; |
|||
import { initializeApp } from './main.common' |
|||
|
|||
const app = initializeApp(); |
|||
app.mount("#app"); |
|||
const app = initializeApp() |
|||
app.mount('#app') |
|||
|
|||
@ -1,5 +1,5 @@ |
|||
import { initializeApp } from "./main.common"; |
|||
import "./registerServiceWorker"; // Web PWA support
|
|||
import { initializeApp } from './main.common' |
|||
import './registerServiceWorker' // Web PWA support
|
|||
|
|||
const app = initializeApp(); |
|||
app.mount("#app"); |
|||
const app = initializeApp() |
|||
app.mount('#app') |
|||
|
|||
@ -1,39 +1,39 @@ |
|||
/* eslint-disable no-console */ |
|||
|
|||
import { register } from "register-service-worker"; |
|||
import { register } from 'register-service-worker' |
|||
|
|||
// Only register service worker if explicitly enabled and in production
|
|||
if ( |
|||
process.env.VITE_PWA_ENABLED === "true" && |
|||
process.env.NODE_ENV === "production" |
|||
process.env.VITE_PWA_ENABLED === 'true' && |
|||
process.env.NODE_ENV === 'production' |
|||
) { |
|||
register(`${process.env.BASE_URL}sw.js`, { |
|||
ready() { |
|||
console.log("Service worker is active."); |
|||
console.log('Service worker is active.') |
|||
}, |
|||
registered() { |
|||
console.log("Service worker has been registered."); |
|||
console.log('Service worker has been registered.') |
|||
}, |
|||
cached() { |
|||
console.log("Content has been cached for offline use."); |
|||
console.log('Content has been cached for offline use.') |
|||
}, |
|||
updatefound() { |
|||
console.log("New content is downloading."); |
|||
console.log('New content is downloading.') |
|||
}, |
|||
updated() { |
|||
console.log("New content is available; please refresh."); |
|||
console.log('New content is available; please refresh.') |
|||
}, |
|||
offline() { |
|||
console.log( |
|||
"No internet connection found. App is running in offline mode.", |
|||
); |
|||
'No internet connection found. App is running in offline mode.' |
|||
) |
|||
}, |
|||
error(error) { |
|||
console.error("Error during service worker registration:", error); |
|||
}, |
|||
}); |
|||
console.error('Error during service worker registration:', error) |
|||
} |
|||
}) |
|||
} else { |
|||
console.log( |
|||
"Service worker registration skipped - not enabled or not in production", |
|||
); |
|||
'Service worker registration skipped - not enabled or not in production' |
|||
) |
|||
} |
|||
|
|||
@ -1,88 +1,88 @@ |
|||
import { |
|||
BarcodeScanner, |
|||
BarcodeFormat, |
|||
LensFacing, |
|||
} from "@capacitor-mlkit/barcode-scanning"; |
|||
import type { PluginListenerHandle } from "@capacitor/core"; |
|||
import { QRScannerService, ScanListener } from "./types"; |
|||
LensFacing |
|||
} from '@capacitor-mlkit/barcode-scanning' |
|||
import type { PluginListenerHandle } from '@capacitor/core' |
|||
import { QRScannerService, ScanListener } from './types' |
|||
|
|||
export class NativeQRScanner implements QRScannerService { |
|||
private scanListener: ScanListener | null = null; |
|||
private isScanning = false; |
|||
private listenerHandle: PluginListenerHandle | null = null; |
|||
private scanListener: ScanListener | null = null |
|||
private isScanning = false |
|||
private listenerHandle: PluginListenerHandle | null = null |
|||
|
|||
async checkPermissions(): Promise<boolean> { |
|||
const { camera } = await BarcodeScanner.checkPermissions(); |
|||
return camera === "granted"; |
|||
const { camera } = await BarcodeScanner.checkPermissions() |
|||
return camera === 'granted' |
|||
} |
|||
|
|||
async requestPermissions(): Promise<boolean> { |
|||
const { camera } = await BarcodeScanner.requestPermissions(); |
|||
return camera === "granted"; |
|||
const { camera } = await BarcodeScanner.requestPermissions() |
|||
return camera === 'granted' |
|||
} |
|||
|
|||
async isSupported(): Promise<boolean> { |
|||
const { supported } = await BarcodeScanner.isSupported(); |
|||
return supported; |
|||
const { supported } = await BarcodeScanner.isSupported() |
|||
return supported |
|||
} |
|||
|
|||
async startScan(): Promise<void> { |
|||
if (this.isScanning) { |
|||
throw new Error("Scanner is already running"); |
|||
throw new Error('Scanner is already running') |
|||
} |
|||
|
|||
try { |
|||
this.isScanning = true; |
|||
this.isScanning = true |
|||
await BarcodeScanner.startScan({ |
|||
formats: [BarcodeFormat.QrCode], |
|||
lensFacing: LensFacing.Back, |
|||
}); |
|||
lensFacing: LensFacing.Back |
|||
}) |
|||
|
|||
this.listenerHandle = await BarcodeScanner.addListener( |
|||
"barcodesScanned", |
|||
'barcodesScanned', |
|||
async (result) => { |
|||
if (result.barcodes.length > 0 && this.scanListener) { |
|||
const barcode = result.barcodes[0]; |
|||
this.scanListener.onScan(barcode.rawValue); |
|||
await this.stopScan(); |
|||
const barcode = result.barcodes[0] |
|||
this.scanListener.onScan(barcode.rawValue) |
|||
await this.stopScan() |
|||
} |
|||
}, |
|||
); |
|||
} |
|||
) |
|||
} catch (error) { |
|||
this.isScanning = false; |
|||
this.isScanning = false |
|||
if (this.scanListener?.onError) { |
|||
this.scanListener.onError(new Error(String(error))); |
|||
this.scanListener.onError(new Error(String(error))) |
|||
} |
|||
throw error; |
|||
throw error |
|||
} |
|||
} |
|||
|
|||
async stopScan(): Promise<void> { |
|||
if (!this.isScanning) { |
|||
return; |
|||
return |
|||
} |
|||
|
|||
try { |
|||
await BarcodeScanner.stopScan(); |
|||
this.isScanning = false; |
|||
await BarcodeScanner.stopScan() |
|||
this.isScanning = false |
|||
} catch (error) { |
|||
if (this.scanListener?.onError) { |
|||
this.scanListener.onError(new Error(String(error))); |
|||
this.scanListener.onError(new Error(String(error))) |
|||
} |
|||
throw error; |
|||
throw error |
|||
} |
|||
} |
|||
|
|||
addListener(listener: ScanListener): void { |
|||
this.scanListener = listener; |
|||
this.scanListener = listener |
|||
} |
|||
|
|||
async cleanup(): Promise<void> { |
|||
await this.stopScan(); |
|||
await this.stopScan() |
|||
if (this.listenerHandle) { |
|||
await this.listenerHandle.remove(); |
|||
this.listenerHandle = null; |
|||
await this.listenerHandle.remove() |
|||
this.listenerHandle = null |
|||
} |
|||
this.scanListener = null; |
|||
this.scanListener = null |
|||
} |
|||
} |
|||
|
|||
@ -1,29 +1,38 @@ |
|||
import { Capacitor } from "@capacitor/core"; |
|||
import { CapacitorQRScanner } from "./CapacitorQRScanner"; |
|||
import { WebQRScanner } from "./WebQRScanner"; |
|||
import type { QRScannerService } from "./types"; |
|||
import { logger } from "../../utils/logger"; |
|||
import { Capacitor } from '@capacitor/core' |
|||
import { CapacitorQRScanner } from './CapacitorQRScanner' |
|||
import type { QRScannerService } from './types' |
|||
import { logger } from '../../utils/logger' |
|||
import { WebDialogQRScanner } from './WebDialogQRScanner' |
|||
|
|||
// Import platform-specific flags from Vite config
|
|||
declare const __USE_QR_READER__: boolean |
|||
declare const __IS_MOBILE__: boolean |
|||
|
|||
export class QRScannerFactory { |
|||
private static instance: QRScannerService | null = null; |
|||
private static instance: QRScannerService | null = null |
|||
|
|||
static getInstance(): QRScannerService { |
|||
if (!this.instance) { |
|||
if (Capacitor.isNativePlatform()) { |
|||
logger.log("Creating native QR scanner instance"); |
|||
this.instance = new CapacitorQRScanner(); |
|||
// Use platform-specific flags for more accurate detection
|
|||
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) { |
|||
logger.log('Creating native QR scanner instance') |
|||
this.instance = new CapacitorQRScanner() |
|||
} else if (__USE_QR_READER__) { |
|||
logger.log('Creating web QR scanner instance') |
|||
this.instance = new WebDialogQRScanner() |
|||
} else { |
|||
logger.log("Creating web QR scanner instance"); |
|||
this.instance = new WebQRScanner(); |
|||
throw new Error( |
|||
'No QR scanner implementation available for this platform' |
|||
) |
|||
} |
|||
} |
|||
return this.instance; |
|||
return this.instance |
|||
} |
|||
|
|||
static async cleanup() { |
|||
if (this.instance) { |
|||
await this.instance.cleanup(); |
|||
this.instance = null; |
|||
await this.instance.cleanup() |
|||
this.instance = null |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,90 @@ |
|||
import type { QRScannerService, ScanListener } from './types' |
|||
import QRScannerDialog from '../../components/QRScannerDialog.vue' |
|||
import { createApp, type App } from 'vue' |
|||
import { logger } from '../../utils/logger' |
|||
|
|||
// Import platform-specific flags from Vite config
|
|||
declare const __USE_QR_READER__: boolean |
|||
|
|||
export class WebDialogQRScanner implements QRScannerService { |
|||
private dialogInstance: App | null = null |
|||
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null |
|||
private scanListener: ScanListener | null = null |
|||
|
|||
async checkPermissions(): Promise<boolean> { |
|||
try { |
|||
const permissions = await navigator.permissions.query({ |
|||
name: 'camera' as PermissionName |
|||
}) |
|||
return permissions.state === 'granted' |
|||
} catch (error) { |
|||
logger.error('Error checking camera permissions:', error) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
async requestPermissions(): Promise<boolean> { |
|||
try { |
|||
const stream = await navigator.mediaDevices.getUserMedia({ |
|||
video: { facingMode: 'environment' } |
|||
}) |
|||
stream.getTracks().forEach((track) => track.stop()) |
|||
return true |
|||
} catch (error) { |
|||
logger.error('Error requesting camera permissions:', error) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
async isSupported(): Promise<boolean> { |
|||
if (!__USE_QR_READER__) { |
|||
return false |
|||
} |
|||
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) |
|||
} |
|||
|
|||
async startScan(): Promise<void> { |
|||
if (!__USE_QR_READER__) { |
|||
throw new Error('Web QR scanner is not supported on this platform') |
|||
} |
|||
|
|||
if (!this.dialogInstance) { |
|||
const div = document.createElement('div') |
|||
document.body.appendChild(div) |
|||
|
|||
this.dialogInstance = createApp(QRScannerDialog) |
|||
this.dialogComponent = this.dialogInstance.mount(div) as InstanceType< |
|||
typeof QRScannerDialog |
|||
> |
|||
} |
|||
|
|||
if (this.dialogComponent && this.scanListener) { |
|||
this.dialogComponent.open((result: string) => { |
|||
if (this.scanListener) { |
|||
this.scanListener.onScan(result) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
async stopScan(): Promise<void> { |
|||
if (this.dialogComponent) { |
|||
this.dialogComponent.close() |
|||
} |
|||
} |
|||
|
|||
addListener(listener: ScanListener): void { |
|||
this.scanListener = listener |
|||
} |
|||
|
|||
async cleanup(): Promise<void> { |
|||
if (this.dialogComponent) { |
|||
this.dialogComponent.close() |
|||
} |
|||
if (this.dialogInstance) { |
|||
this.dialogInstance.unmount() |
|||
this.dialogInstance = null |
|||
this.dialogComponent = null |
|||
} |
|||
} |
|||
} |
|||
@ -1,142 +0,0 @@ |
|||
import jsQR from "jsqr"; |
|||
import { QRScannerService, ScanListener } from "./types"; |
|||
import { logger } from "../../utils/logger"; |
|||
|
|||
export class WebQRScanner implements QRScannerService { |
|||
private video: HTMLVideoElement | null = null; |
|||
private canvas: HTMLCanvasElement | null = null; |
|||
private context: CanvasRenderingContext2D | null = null; |
|||
private animationFrameId: number | null = null; |
|||
private scanListener: ScanListener | null = null; |
|||
private isScanning = false; |
|||
private mediaStream: MediaStream | null = null; |
|||
|
|||
constructor() { |
|||
this.video = document.createElement("video"); |
|||
this.canvas = document.createElement("canvas"); |
|||
this.context = this.canvas.getContext("2d"); |
|||
} |
|||
|
|||
async checkPermissions(): Promise<boolean> { |
|||
try { |
|||
const permissions = await navigator.permissions.query({ |
|||
name: "camera" as PermissionName, |
|||
}); |
|||
return permissions.state === "granted"; |
|||
} catch (error) { |
|||
logger.error("Error checking camera permissions:", error); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
async requestPermissions(): Promise<boolean> { |
|||
try { |
|||
const stream = await navigator.mediaDevices.getUserMedia({ |
|||
video: { facingMode: "environment" }, |
|||
}); |
|||
stream.getTracks().forEach((track) => track.stop()); |
|||
return true; |
|||
} catch (error) { |
|||
logger.error("Error requesting camera permissions:", error); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
async isSupported(): Promise<boolean> { |
|||
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia); |
|||
} |
|||
|
|||
async startScan(): Promise<void> { |
|||
if (this.isScanning || !this.video || !this.canvas || !this.context) { |
|||
throw new Error("Scanner is already running or not properly initialized"); |
|||
} |
|||
|
|||
try { |
|||
this.mediaStream = await navigator.mediaDevices.getUserMedia({ |
|||
video: { facingMode: "environment" }, |
|||
}); |
|||
|
|||
this.video.srcObject = this.mediaStream; |
|||
this.video.setAttribute("playsinline", "true"); |
|||
await this.video.play(); |
|||
|
|||
this.canvas.width = this.video.videoWidth; |
|||
this.canvas.height = this.video.videoHeight; |
|||
|
|||
this.isScanning = true; |
|||
this.scanFrame(); |
|||
} catch (error) { |
|||
logger.error("Error starting scan:", error); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
private scanFrame = () => { |
|||
if ( |
|||
!this.isScanning || |
|||
!this.video || |
|||
!this.canvas || |
|||
!this.context || |
|||
!this.scanListener |
|||
) { |
|||
return; |
|||
} |
|||
|
|||
if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) { |
|||
this.canvas.width = this.video.videoWidth; |
|||
this.canvas.height = this.video.videoHeight; |
|||
this.context.drawImage( |
|||
this.video, |
|||
0, |
|||
0, |
|||
this.canvas.width, |
|||
this.canvas.height, |
|||
); |
|||
|
|||
const imageData = this.context.getImageData( |
|||
0, |
|||
0, |
|||
this.canvas.width, |
|||
this.canvas.height, |
|||
); |
|||
const code = jsQR(imageData.data, imageData.width, imageData.height, { |
|||
inversionAttempts: "dontInvert", |
|||
}); |
|||
|
|||
if (code) { |
|||
this.scanListener.onScan(code.data); |
|||
} |
|||
} |
|||
|
|||
this.animationFrameId = requestAnimationFrame(this.scanFrame); |
|||
}; |
|||
|
|||
async stopScan(): Promise<void> { |
|||
this.isScanning = false; |
|||
if (this.animationFrameId) { |
|||
cancelAnimationFrame(this.animationFrameId); |
|||
this.animationFrameId = null; |
|||
} |
|||
|
|||
if (this.mediaStream) { |
|||
this.mediaStream.getTracks().forEach((track) => track.stop()); |
|||
this.mediaStream = null; |
|||
} |
|||
|
|||
if (this.video) { |
|||
this.video.srcObject = null; |
|||
} |
|||
} |
|||
|
|||
addListener(listener: ScanListener): void { |
|||
this.scanListener = listener; |
|||
} |
|||
|
|||
async cleanup(): Promise<void> { |
|||
await this.stopScan(); |
|||
this.scanListener = null; |
|||
this.video = null; |
|||
this.canvas = null; |
|||
this.context = null; |
|||
} |
|||
} |
|||
@ -1,29 +1,29 @@ |
|||
export interface ScanResult { |
|||
rawValue: string; |
|||
rawValue: string |
|||
} |
|||
|
|||
export interface QRScannerState { |
|||
isSupported: boolean; |
|||
granted: boolean; |
|||
denied: boolean; |
|||
isProcessing: boolean; |
|||
processingStatus: string; |
|||
processingDetails: string; |
|||
error: string; |
|||
status: string; |
|||
isSupported: boolean |
|||
granted: boolean |
|||
denied: boolean |
|||
isProcessing: boolean |
|||
processingStatus: string |
|||
processingDetails: string |
|||
error: string |
|||
status: string |
|||
} |
|||
|
|||
export interface ScanListener { |
|||
onScan: (result: string) => void; |
|||
onError?: (error: Error) => void; |
|||
onScan: (result: string) => void |
|||
onError?: (error: Error) => void |
|||
} |
|||
|
|||
export interface QRScannerService { |
|||
checkPermissions(): Promise<boolean>; |
|||
requestPermissions(): Promise<boolean>; |
|||
isSupported(): Promise<boolean>; |
|||
startScan(): Promise<void>; |
|||
stopScan(): Promise<void>; |
|||
addListener(listener: ScanListener): void; |
|||
cleanup(): Promise<void>; |
|||
checkPermissions(): Promise<boolean> |
|||
requestPermissions(): Promise<boolean> |
|||
isSupported(): Promise<boolean> |
|||
startScan(): Promise<void> |
|||
stopScan(): Promise<void> |
|||
addListener(listener: ScanListener): void |
|||
cleanup(): Promise<void> |
|||
} |
|||
|
|||
@ -1,62 +1,62 @@ |
|||
import axios from "axios"; |
|||
import * as didJwt from "did-jwt"; |
|||
import { AppString } from "../constants/app"; |
|||
import { retrieveSettingsForActiveAccount } from "../db"; |
|||
import { SERVICE_ID } from "../libs/endorserServer"; |
|||
import { deriveAddress, newIdentifier } from "../libs/crypto"; |
|||
import { logger } from "../utils/logger"; |
|||
import axios from 'axios' |
|||
import * as didJwt from 'did-jwt' |
|||
import { AppString } from '../constants/app' |
|||
import { retrieveSettingsForActiveAccount } from '../db' |
|||
import { SERVICE_ID } from '../libs/endorserServer' |
|||
import { deriveAddress, newIdentifier } from '../libs/crypto' |
|||
import { logger } from '../utils/logger' |
|||
/** |
|||
* Get User #0 to sign & submit a RegisterAction for the user's activeDid. |
|||
*/ |
|||
export async function testServerRegisterUser() { |
|||
const testUser0Mnem = |
|||
"seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control"; |
|||
'seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control' |
|||
|
|||
const [addr, privateHex, publicHex, deriPath] = deriveAddress(testUser0Mnem); |
|||
const [addr, privateHex, publicHex, deriPath] = deriveAddress(testUser0Mnem) |
|||
|
|||
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath); |
|||
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath) |
|||
|
|||
const settings = await retrieveSettingsForActiveAccount(); |
|||
const settings = await retrieveSettingsForActiveAccount() |
|||
|
|||
// Make a claim
|
|||
const vcClaim = { |
|||
"@context": "https://schema.org", |
|||
"@type": "RegisterAction", |
|||
'@context': 'https://schema.org', |
|||
'@type': 'RegisterAction', |
|||
agent: { did: identity0.did }, |
|||
object: SERVICE_ID, |
|||
participant: { did: settings.activeDid }, |
|||
}; |
|||
participant: { did: settings.activeDid } |
|||
} |
|||
// Make a payload for the claim
|
|||
const vcPayload = { |
|||
sub: "RegisterAction", |
|||
sub: 'RegisterAction', |
|||
vc: { |
|||
"@context": ["https://www.w3.org/2018/credentials/v1"], |
|||
type: ["VerifiableCredential"], |
|||
credentialSubject: vcClaim, |
|||
}, |
|||
}; |
|||
'@context': ['https://www.w3.org/2018/credentials/v1'], |
|||
type: ['VerifiableCredential'], |
|||
credentialSubject: vcClaim |
|||
} |
|||
} |
|||
// create a signature using private key of identity
|
|||
// eslint-disable-next-line
|
|||
const privateKeyHex: string = identity0.keys[0].privateKeyHex!; |
|||
const signer = await didJwt.SimpleSigner(privateKeyHex); |
|||
const alg = undefined; |
|||
const signer = await didJwt.SimpleSigner(privateKeyHex) |
|||
const alg = undefined |
|||
// create a JWT for the request
|
|||
const vcJwt: string = await didJwt.createJWT(vcPayload, { |
|||
alg: alg, |
|||
issuer: identity0.did, |
|||
signer: signer, |
|||
}); |
|||
signer: signer |
|||
}) |
|||
|
|||
// Make the xhr request payload
|
|||
|
|||
const payload = JSON.stringify({ jwtEncoded: vcJwt }); |
|||
const payload = JSON.stringify({ jwtEncoded: vcJwt }) |
|||
const endorserApiServer = |
|||
settings.apiServer || AppString.TEST_ENDORSER_API_SERVER; |
|||
const url = endorserApiServer + "/api/claim"; |
|||
settings.apiServer || AppString.TEST_ENDORSER_API_SERVER |
|||
const url = endorserApiServer + '/api/claim' |
|||
const headers = { |
|||
"Content-Type": "application/json", |
|||
}; |
|||
'Content-Type': 'application/json' |
|||
} |
|||
|
|||
const resp = await axios.post(url, payload, { headers }); |
|||
logger.log("User registration result:", resp); |
|||
const resp = await axios.post(url, payload, { headers }) |
|||
logger.log('User registration result:', resp) |
|||
} |
|||
|
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue