We will build a PHP app for querying the eBay API, and then we will build a web app for getting the barcode and sending it to our API

John Au-Yeung
Sep 10 · 8 min read
Photo by david pl on Unsplash

eBay has a free API for accessing their listing data, and HTML has a camera API for getting images from a camera via a web page. This means that we can create apps that scan barcodes from your web app, get the code, and send it to the eBay API for querying.

In this story, we will build a PHP app for querying the eBay API, and then we will build a web app for getting the barcode and sending it to our API. The back end is simple. It is just a script for getting data from the eBay API via the ISBN code. Create a folder for the back-end app and put in the following:

<?php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use GuzzleHttp\Client;
$ebay = $app['controllers_factory'];
$ebayAppId = $_ENV['EBAY_APP_ID'];
$client = new Client([
'base_uri' => 'http://svcs.ebay.com'
]);
$ebay->get('/find-by-code/{code}/{page}', function ($code, $page) use ($app, $client, $ebayAppId) {
if (strlen($code) == 10 || strlen($code) == 13){
$type = 'ISBN';
}
else if (strlen($code) == 12){
$type = 'UPC';
}
else{
return $app->json(['error' => 'invalid code'], 400);
}
if (!is_int(intval($page)) || $page <= 0){
return $app->json(['error' => 'invalid page'], 400);
}
$response = $client->request('GET', "/services/search/FindingService/v1?OPERATION-NAME=findItemsByProduct&SERVICE-VERSION=1.0.0&SECURITY-APPNAME=$ebayAppId&RESPONSE-DATA-FORMAT=JSON&REST-PAYLOAD&paginationInput.entriesPerPage=10&productId.@type=$type&productId=$code&paginationInput.pageNumber=$page");
return $app->json(json_decode($response->getBody(), true));
});
return $ebay;

We call that ebay.php. The function ($code, $page) use ($app, $client, $ebayAppId) part allows the route to access outside variables since the callback is in a different scope that the outside variables.

Then in index.php, we put

<?php
require_once 'vendor/autoload.php';
$app = new Silex\Application();
$dotenv = new Dotenv\Dotenv('.');
$dotenv->load();
$app->register(new JDesrosiers\Silex\Provider\CorsServiceProvider(), [
"cors.allowOrigin" => "*",
]);
$app['debug']= true;$app->get('/hello/{name}', function($name) use($app) {
return 'Hello '.$app->escape($name);
});
$app->mount('/ebay', include 'ebay.php');
$app["cors-enabled"]($app);
$app->run();

so that we can access our routes.

In composer.json, we put

{
"require": {
"silex/silex": "^2.0",
"vlucas/phpdotenv": "^2.4",
"jdesrosiers/silex-cors-provider": "~1.0",
"guzzlehttp/guzzle": "~6.0"
}
}

so we can run composer install to install our dependencies if Composer is installed.

Now that we have the back end done. We can do the front end. The app is going to be built with Angular. We scaffold the app with the Angular CLI. We run ng new frontend to scaffold the app.

The only things different from our typical apps is that HTTPS is required to access the camera, so we have to generate our own HTTPS certificate for our development server. We should have theserver.crt and server.key file in the same folder as the front-end app files.

The serve section of our angular.json should have:

"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "frontend:build",
"sslKey": "server.key",
"sslCert": "server.cert"
},
"configurations": {
"production": {
"browserTarget": "frontend:build:production"
},
"mobile": {
"browserTarget": "frontend:build:mobile"
}
}
},

where

"sslKey": "server.key",
"sslCert": "server.cert"

should be referencing the path of your certificates.

Then to run the Angular CLI development server, we run:

ng serve --ssl

When you go to https://localhost:4200, you will see an insecure connection error in most browsers. Click proceed to continue.

If your camera is on your Android device, we can debug remotely. In Chrome, press F12 to open the console, click on the top-right menu with the three vertical dots. Then click Remote Devices, connect your Android device to your computer, and enable remote debugging according to the instructions.

Instead of running ng serve --ssl, you run ng serve --ssl --host 0.0.0.0

Once all that is done, you should see the following:

once the app is built.

We install a library for accessing the device’s camera and our flux store by running:

npm i @zxing/ngx-scanner @ngrx/store @angular/material @angular/cdk

In app.module.ts, we put:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ZXingScannerModule } from '@zxing/ngx-scanner';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatTabsModule } from '@angular/material/tabs';
import { MatCardModule } from '@angular/material/card';
import { HttpClientModule } from '@angular/common/http';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTableModule } from '@angular/material/table';
import { MatSelectModule } from '@angular/material/select';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomePageComponent } from './home-page/home-page.component';
import { StoreModule } from '@ngrx/store';
import { barCodeReducer } from './bar-code-reducer';
import { FormsModule } from '@angular/forms';
import { EbayTabComponent } from './ebay-tab/ebay-tab.component';
@NgModule({
declarations: [
AppComponent,
HomePageComponent,
EbayTabComponent
],
imports: [
BrowserModule,
AppRoutingModule,
ZXingScannerModule,
BrowserAnimationsModule,
MatButtonModule,
MatToolbarModule,
MatInputModule,
MatTabsModule,
StoreModule.forRoot({ barCode: barCodeReducer }),
FormsModule,
MatCardModule,
HttpClientModule,
MatPaginatorModule,
MatTableModule,
MatSelectModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

This incorporates the Angular Material components and our flux store.

Now we have to make a centralized store for our data. We create a file called bar-code-reducer.ts and add the following:

export const SET_BARCODE = 'SET_BARCODE';export function barCodeReducer(state: string = '', action) {
switch (action.type) {
case SET_BARCODE:
return action.payload;
default:
return state;
}
}

Now we can add our front-end components. We run:

ng g component ebayTab
ng g component homePage

This adds the page to display our barcode scanner and a section to display our eBay data.

Next, we create a service to create our HTTP request by running:

ng g service productSearch

After that, we should have produce-search.service.ts. We put the following in there:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class ProductSearchService {
constructor(
private http: HttpClient
) { }
searchProduct(barcode: string, currentPage: number) {
return this.http.get(`${environment.apiUrl}/ebay/find-by-code/${barcode}/${currentPage}`)
}
}

In ebay-tab.component.ts, we put:

import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { ProductSearchService } from '../product-search.service';
import { ConstantPool } from '@angular/compiler';
@Component({
selector: 'app-ebay-tab',
templateUrl: './ebay-tab.component.html',
styleUrls: ['./ebay-tab.component.css']
})
export class EbayTabComponent implements OnInit {
barcode$: Observable<string>;
barcodeValue: string;
products: any[] = [];
totalPages: number = 0;
totalEntries: number = 0;
entriesPerPage: number = 0;
currentPage: number = 1;
displayedColumns: string[] = [
'itemId',
'title',
'location',
'country',
'shippingServiceCost',
'currentPrice',
'convertedCurrentPrice',
'bestOfferEnabled',
'buyItNowAvailable',
'listingType'
];
constructor(
private store: Store<any>,
private productSearchService: ProductSearchService
) {
this.barcode$ = store.pipe(select('barCode'))
this.barcode$.subscribe(barcode => {
this.barcodeValue = barcode;
this.products = [];
this.searchProduct(this.barcodeValue, this.currentPage);
}, err => {
})
}
ngOnInit() {
}
searchProduct(barcodeValue, currentPage) {
this.productSearchService.searchProduct(barcodeValue, currentPage)
.subscribe((res: any) => {
try {
this.products = res.findItemsByProductResponse[0].searchResult[0].item as any[];
this.products = this.products.map(p => {
let shippingServiceCost = p.shippingInfo[0].shippingServiceCost;
let sellingStatus = p.sellingStatus;
return {
itemId: p.itemId,
title: p.title,
country: p.country,
location: p.location,
shippingServiceCost: Array.isArray(shippingServiceCost) ? `${shippingServiceCost[0]['__value__']} ${shippingServiceCost[0]['@currencyId']}` : '',
currentPrice: Array.isArray(sellingStatus) ? `${sellingStatus[0].currentPrice[0]['__value__']} ${sellingStatus[0].currentPrice[0]['@currencyId']}` : '',
convertedCurrentPrice: Array.isArray(sellingStatus) ? `${sellingStatus[0].convertedCurrentPrice[0]['__value__']} ${sellingStatus[0].convertedCurrentPrice[0]['@currencyId']}` : '',
bestOfferEnabled: p.listingInfo[0].bestOfferEnabled[0],
buyItNowAvailable: p.listingInfo[0].buyItNowAvailable[0],
listingType: p.listingInfo[0].listingType[0]
}
})
this.totalPages = res.findItemsByProductResponse[0].paginationOutput[0].totalPages[0];
this.totalEntries = res.findItemsByProductResponse[0].paginationOutput[0].totalEntries[0];
this.entriesPerPage = res.findItemsByProductResponse[0].paginationOutput[0].entriesPerPage[0];
}
catch (ex) {
this.products = [];
}
}, err => {
this.products = [];
})
}
getProducts(event) {
this.currentPage = event.pageIndex + 1;
this.searchProduct(this.barcodeValue, this.currentPage);
}
}

And in ebay-tab.component.html, we have:

<div *ngIf='products.length > 0'>
<table mat-table [dataSource]="products" class="mat-elevation-z8">
<ng-container matColumnDef="itemId">
<th mat-header-cell *matHeaderCellDef> Item ID </th>
<td mat-cell *matCellDef="let element"> {{element.itemId}} </td>
</ng-container>
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef> Title </th>
<td mat-cell *matCellDef="let element"> {{element.title}} </td>
</ng-container>
<ng-container matColumnDef="location">
<th mat-header-cell *matHeaderCellDef> Location </th>
<td mat-cell *matCellDef="let element"> {{element.location}} </td>
</ng-container>
<ng-container matColumnDef="country">
<th mat-header-cell *matHeaderCellDef> Country </th>
<td mat-cell *matCellDef="let element"> {{element.country}} </td>
</ng-container>
<ng-container matColumnDef="shippingServiceCost">
<th mat-header-cell *matHeaderCellDef> Shipping Cost </th>
<td mat-cell *matCellDef="let element"> {{element.shippingServiceCost}} </td>
</ng-container>
<ng-container matColumnDef="currentPrice">
<th mat-header-cell *matHeaderCellDef> Current Price </th>
<td mat-cell *matCellDef="let element"> {{element.currentPrice}} </td>
</ng-container>
<ng-container matColumnDef="convertedCurrentPrice">
<th mat-header-cell *matHeaderCellDef> Converted Current Price </th>
<td mat-cell *matCellDef="let element"> {{element.convertedCurrentPrice}} </td>
</ng-container>
<ng-container matColumnDef="bestOfferEnabled">
<th mat-header-cell *matHeaderCellDef> Best Offer Enabled </th>
<td mat-cell *matCellDef="let element"> {{element.bestOfferEnabled}} </td>
</ng-container>
<ng-container matColumnDef="buyItNowAvailable">
<th mat-header-cell *matHeaderCellDef> Buy It Now </th>
<td mat-cell *matCellDef="let element"> {{element.buyItNowAvailable}} </td>
</ng-container>
<ng-container matColumnDef="listingType">
<th mat-header-cell *matHeaderCellDef> Listing Type </th>
<td mat-cell *matCellDef="let element"> {{element.listingType}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [length]="totalEntries" [pageSize]="entriesPerPage" (page)='getProducts($event)'>
</mat-paginator>
</div>
<div *ngIf='products.length == 0' class="center">
<h1>No Results</h1>
</div>

In home-page.component.ts, we put:

import { Component, OnInit, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { SET_BARCODE } from '../bar-code-reducer';
import { NgForm } from '@angular/forms';
import { BarcodeFormat } from '@zxing/library';
import { ZXingScannerComponent } from '@zxing/ngx-scanner';
@Component({
selector: 'app-home-page',
templateUrl: './home-page.component.html',
styleUrls: ['./home-page.component.css']
})
export class HomePageComponent implements OnInit {
barcodeValue: number;
webCamAvailable: boolean = true;
barcode$: Observable<string>;
searching: boolean = false;
allowedFormats = [
BarcodeFormat.QR_CODE,
BarcodeFormat.EAN_13,
BarcodeFormat.CODE_128,
BarcodeFormat.DATA_MATRIX,
BarcodeFormat.UPC_A,
BarcodeFormat.UPC_E,
BarcodeFormat.UPC_EAN_EXTENSION,
BarcodeFormat.CODABAR,
BarcodeFormat.CODE_39,
BarcodeFormat.CODE_93
];
hasCameras = false;
hasPermission: boolean;
qrResultString: string;
availableDevices: MediaDeviceInfo[];
selectedDevice: MediaDeviceInfo;
@ViewChild('scanner')
scanner: ZXingScannerComponent;
constructor(
private store: Store<any>
) {
} ngOnInit() {
this.scanner.camerasFound.subscribe((devices: MediaDeviceInfo[]) => {
this.hasCameras = true;
this.availableDevices = devices;
});this.scanner.permissionResponse.subscribe((answer: boolean) => {
this.hasPermission = answer;
});
}
onValueChanges(result) {
this.barcodeValue = result.codeResult.code;
this.searching = true;
this.store.dispatch({ type: SET_BARCODE, payload: this.barcodeValue });
}
searchProduct(barCodeForm: NgForm) {
this.searching = false;
if (barCodeForm.invalid) {
return;
}
this.searching = true;
this.store.dispatch({ type: SET_BARCODE, payload: this.barcodeValue });
}
scanSuccessHandler(event) {
console.log(event);
this.barcodeValue = event;
this.store.dispatch({ type: SET_BARCODE, payload: this.barcodeValue });
}
onDeviceSelectChange(selectedValue: string) {
this.selectedDevice = this.scanner.getDeviceById(selectedValue);
}
scanErrorHandler(event) {
console.log(event);
}
scanFailureHandler(event) {
console.log(event);
}
scanCompleteHandler(event) {
console.log(event);
this.barcodeValue = event.text;
this.store.dispatch({ type: SET_BARCODE, payload: this.barcodeValue });
}
}

In addition to scanning, you can also enter the barcode manually. If a camera is present, you should see a preview box. After scanning, we get the barcode value and propagate that to the ebay-tab component that we created via the flux store.

Better Programming

Advice for programmers.

John Au-Yeung

Written by

Subscribe to my email list now at http://jauyeung.net/subscribe/ . Follow me on Twitter at https://twitter.com/AuMayeung

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade