Skip to content

Commit

Permalink
feat: NFC support for canteen cards to query the balance
Browse files Browse the repository at this point in the history
  • Loading branch information
robingenz committed Mar 1, 2024
1 parent 4335129 commit a4de1e6
Show file tree
Hide file tree
Showing 23 changed files with 623 additions and 4 deletions.
1 change: 0 additions & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
save-exact=true

@capawesome-team:registry=https://npm.pkg.github.com
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@capacitor/splash-screen": "5.0.7",
"@capacitor/status-bar": "5.0.7",
"@capawesome-team/capacitor-file-opener": "5.0.4",
"@capawesome-team/capacitor-nfc": "5.1.0-dev.7d38c01.1709239536",
"@fortawesome/angular-fontawesome": "0.14.1",
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-brands-svg-icons": "6.5.1",
Expand Down
7 changes: 6 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { registerLocaleData } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { ErrorHandler, NgModule } from '@angular/core';
import localeDe from '@angular/common/locales/de';
import { ErrorHandler, LOCALE_ID, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { CoreModule, GlobalErrorHandlerService } from '@app/core';
Expand All @@ -11,6 +13,8 @@ import { register as registerSwiper } from 'swiper/element/bundle';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

registerLocaleData(localeDe);

registerSwiper();

@NgModule({
Expand All @@ -34,6 +38,7 @@ registerSwiper();
provide: ErrorHandler,
useClass: GlobalErrorHandlerService,
},
{ provide: LOCALE_ID, useValue: 'de-DE' },
],
bootstrap: [AppComponent],
})
Expand Down
111 changes: 111 additions & 0 deletions src/app/core/services/capacitor/capacitor-nfc/capacitor-nfc.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Injectable, NgZone } from '@angular/core';
import { Capacitor } from '@capacitor/core';
import {
ConnectOptions,
Nfc,
NfcTag,
PollingOption,
TransceiveOptions,
TransceiveResult,
WriteOptions,
} from '@capawesome-team/capacitor-nfc';
import { Observable, ReplaySubject, Subject } from 'rxjs';

@Injectable({
providedIn: 'root',
})
export class CapacitorNfcService {
private readonly scannedTagSubject = new Subject<NfcTag>();
private readonly lastScannedTagSubject = new ReplaySubject<NfcTag>(1);
private readonly sessionCanceledSubject = new Subject<void>();
private readonly sessionErrorSubject = new Subject<string>();

constructor(private readonly ngZone: NgZone) {
void Nfc.removeAllListeners().then(() => {
void Nfc.addListener('nfcTagScanned', event => {
this.ngZone.run(() => {
this.scannedTagSubject.next(event.nfcTag);
this.lastScannedTagSubject.next(event.nfcTag);
});
});
void Nfc.addListener('scanSessionCanceled', () => {
this.ngZone.run(() => {
this.sessionCanceledSubject.next();
});
});
void Nfc.addListener('scanSessionError', event => {
this.ngZone.run(() => {
this.sessionErrorSubject.next(event.message);
});
});
});
}

public get scannedTag$(): Observable<NfcTag> {
return this.scannedTagSubject.asObservable();
}

public get lastScannedTag$(): Observable<NfcTag> {
return this.lastScannedTagSubject.asObservable();
}

public get sessionCanceled$(): Observable<void> {
return this.sessionCanceledSubject.asObservable();
}

public get sessionError$(): Observable<string> {
return this.sessionErrorSubject.asObservable();
}

public async startScanSession(): Promise<void> {
await Nfc.startScanSession({
pollingOptions: [PollingOption.iso14443, PollingOption.iso15693],
});
}

public async stopScanSession(): Promise<void> {
await Nfc.stopScanSession();
}

public async write(options: WriteOptions): Promise<void> {
await Nfc.write(options);
}

public async erase(): Promise<void> {
await Nfc.erase();
}

public async format(): Promise<void> {
await Nfc.format();
}

public transceive(options: TransceiveOptions): Promise<TransceiveResult> {
return Nfc.transceive(options);
}

public connect(options: ConnectOptions): Promise<void> {
return Nfc.connect(options);
}

public async close(): Promise<void> {
await Nfc.close();
}

public async openSettings(): Promise<void> {
await Nfc.openSettings();
}

public async isSupported(): Promise<boolean> {
const { isSupported } = await Nfc.isSupported();
return isSupported;
}

public async isEnabled(): Promise<boolean> {
const isAndroid = Capacitor.getPlatform() === 'android';
if (!isAndroid) {
return true;
}
const { isEnabled } = await Nfc.isEnabled();
return isEnabled;
}
}
1 change: 1 addition & 0 deletions src/app/core/services/capacitor/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './capacitor-app/capacitor-app.service';
export * from './capacitor-nfc/capacitor-nfc.service';
2 changes: 2 additions & 0 deletions src/app/core/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export * from './capacitor';
export * from './dialog/dialog.service';
export * from './global-error-handler/global-error-handler.service';
export * from './native-http/native-http.service';
export * from './nfc';
export * from './notification/notification.service';
export * from './storage/storage.service';
export * from './timeout/timeout.service';
export * from './user/user.service';
2 changes: 2 additions & 0 deletions src/app/core/services/nfc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './nfc-helper/nfc-helper.service';
export * from './nfc/nfc.service';
23 changes: 23 additions & 0 deletions src/app/core/services/nfc/nfc-helper/nfc-helper.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';
import { NfcUtils } from '@capawesome-team/capacitor-nfc';

@Injectable({
providedIn: 'root',
})
export class NfcHelperService {
private readonly nfcUtilsInstance = new NfcUtils();

constructor() {}

public convertHexToBytes(hex: string): number[] {
return this.nfcUtilsInstance.convertHexToBytes({ hex }).bytes;
}

public convertBytesToHex(bytes: number[]): string {
return this.nfcUtilsInstance.convertBytesToHex({ bytes }).hex;
}

public convertHexToNumber(hex: string): number {
return this.nfcUtilsInstance.convertHexToNumber({ hex }).number;
}
}
141 changes: 141 additions & 0 deletions src/app/core/services/nfc/nfc/nfc.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Injectable } from '@angular/core';
import { NdefMessage, NfcTag, NfcTagTechType } from '@capawesome-team/capacitor-nfc';
import { Observable } from 'rxjs';
import { CapacitorNfcService } from '../../capacitor';

@Injectable({
providedIn: 'root',
})
export class NfcService {
constructor(private readonly capacitorNfcService: CapacitorNfcService) {}

public get scannedTag$(): Observable<NfcTag> {
return this.capacitorNfcService.scannedTag$;
}

public get lastScannedTag$(): Observable<NfcTag> {
return this.capacitorNfcService.lastScannedTag$;
}

public async startScanSession(): Promise<void> {
const isSupported = await this.isSupported();
if (!isSupported) {
throw this.createNotSupportedError();
}
const isEnabled = await this.isEnabled();
if (!isEnabled) {
throw this.createNotEnabledError();
}
await this.capacitorNfcService.startScanSession();
}

public async stopScanSession(): Promise<void> {
const isSupported = await this.isSupported();
if (!isSupported) {
return;
}
const isEnabled = await this.isEnabled();
if (!isEnabled) {
return;
}
await this.capacitorNfcService.stopScanSession();
}

public async write(message: NdefMessage): Promise<void> {
const isSupported = await this.isSupported();
if (!isSupported) {
throw this.createNotSupportedError();
}
const isEnabled = await this.isEnabled();
if (!isEnabled) {
throw this.createNotEnabledError();
}
await this.capacitorNfcService.write({
message,
});
}

public async erase(): Promise<void> {
const isSupported = await this.isSupported();
if (!isSupported) {
throw this.createNotSupportedError();
}
const isEnabled = await this.isEnabled();
if (!isEnabled) {
throw this.createNotEnabledError();
}
await this.capacitorNfcService.erase();
}

public async format(): Promise<void> {
const isSupported = await this.isSupported();
if (!isSupported) {
throw this.createNotSupportedError();
}
const isEnabled = await this.isEnabled();
if (!isEnabled) {
throw this.createNotEnabledError();
}
await this.capacitorNfcService.format();
}

public async transceive(techType: NfcTagTechType, data: number[]): Promise<number[]> {
const isSupported = await this.isSupported();
if (!isSupported) {
throw this.createNotSupportedError();
}
const isEnabled = await this.isEnabled();
if (!isEnabled) {
throw this.createNotEnabledError();
}
const { response } = await this.capacitorNfcService.transceive({
techType,
data,
});
return response;
}

public async connect(techType: NfcTagTechType): Promise<void> {
const isSupported = await this.isSupported();
if (!isSupported) {
throw this.createNotSupportedError();
}
const isEnabled = await this.isEnabled();
if (!isEnabled) {
throw this.createNotEnabledError();
}
await this.capacitorNfcService.connect({
techType,
});
}

public async close(): Promise<void> {
const isSupported = await this.isSupported();
if (!isSupported) {
return;
}
const isEnabled = await this.isEnabled();
if (!isEnabled) {
return;
}
await this.capacitorNfcService.close();
}

public isSupported(): Promise<boolean> {
return this.capacitorNfcService.isSupported();
}

public isEnabled(): Promise<boolean> {
return this.capacitorNfcService.isEnabled();
}

private createNotSupportedError(): Error {
const message = 'Dein Gerät unterstützt kein NFC.';
return new Error(message);
}

private createNotEnabledError(): Error {
const message = 'Bitte aktiviere NFC in den Einstellungen deines Geräts.';
return new Error(message);
}
}
15 changes: 15 additions & 0 deletions src/app/core/services/timeout/timeout.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class TimeoutService {
public timeout<T>(callback: () => T, timeoutMs: number): Promise<T> {
return new Promise(resolve => {
setTimeout(() => {
const result = callback();
resolve(result);
}, timeoutMs);
});
}
}
Loading

0 comments on commit a4de1e6

Please sign in to comment.