中安拓也のブログ

プログラミングについて書くブログ

Angular + Firebase でTwitter認証

はじめに

AngularとFirebaseを使用して、Twitter認証機能を実装します。

なお、AngularとFirebaseによるメールアドレスとパスワードの認証については実装ずみで、下記の記事で説明もしています。

AngularでFirebase認証(その1) Firebaseのセットアップ - 中安拓也のブログ

AngularでFirebase認証(その2) Angular Materialを使ったログイン画面の作成 - 中安拓也のブログ

AngularでFirebase認証(その3) Firebase Authentication の呼び出し - 中安拓也のブログ

Angular + Firebase でアカウント登録画面の作成 - 中安拓也のブログ

環境

フロントエンドのフレームワークにはAngular/TypeScriptを、CSSフレームワークにはAngular Materialを使用しています。

Firebase関連のライブラリのバージョンは下記となります。

  • firebase@6.3.4
  • angular/fire@5.2.1

また、$ ng --versionの実行結果は下記の通りです。

$ ng --version

Angular CLI: 8.3.20
Node: 12.13.1
OS: darwin x64
Angular: 8.2.14
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.803.20
@angular-devkit/build-angular     0.803.20
@angular-devkit/build-optimizer   0.803.20
@angular-devkit/build-webpack     0.803.20
@angular-devkit/core              8.3.20
@angular-devkit/schematics        8.3.20
@angular/cdk                      7.3.7
@angular/cli                      8.3.20
@angular/fire                     5.2.3
@angular/material                 7.3.7
@ngtools/webpack                  8.3.20
@schematics/angular               8.3.20
@schematics/update                0.803.20
rxjs                              6.5.3
typescript                        3.5.3
webpack                           4.39.2

TwitterのAPIキーとAPIシークレットを取得する

f:id:l08084:20200409011846p:plain
Create App をクリック

Twitterの開発者向けサイトに移動して、Create an appボタンをクリックします。

Twitter Developer アカウントの作成

Twitter Developer Accountを持っていない場合は、Twitter Developer Accountの作成ページに自動で遷移します。

どのような目的でTwitter APIを使用するのか?などの質問をされるので英語で回答します。

すべての質問に回答すると下記のような画面が表示された後、本人確認用リンクが添付されたメールが送信されるので、本人確認用リンクをクリックするとTwitter Developer Accountの登録が完了します。

f:id:l08084:20200411225849p:plain

アプリの登録

再度Twitterの開発者向けサイトに移動して、Create an appボタンをクリックします。すると、下記のような画面が表示されるので回答していきます。

f:id:l08084:20200411230341p:plain

Website URLもCallback URLsも現時点では入力できないので、適用にやり過ごしてあとで正しい値を設定するようにします。

回答が完了すると下記の画面を閲覧できるようになり、APIキーとAPIシークレットを取得できます。

f:id:l08084:20200411234429p:plain

FirebaseコンソールでTwitter認証を有効にする

Firebaseコンソールに移動してTwitterを認証方法として有効にします。なお、有効にする際には先ほど取得したAPIキーとAPIシークレットを入力する必要があります。

f:id:l08084:20200407224024p:plain
Twitterを認証方法として有効にする

また、Firebaseコンソールに表示されているコールバックURLもTwitterの開発者向けサイトに登録したアプリに設定する必要があります。

f:id:l08084:20200412001400p:plain
コールバックURLをコピーする

f:id:l08084:20200412001733p:plain
コールバックURLを設定する

これで「Firebase構成オブジェクトの転記」が完了してる場合は、Firebase側の設定はすべて完了です。「Firebase構成オブジェクトの転記」が完了していない場合は、下記の記事を参考にしてやってみてください。

AngularでFirebase認証(その1) Firebaseのセットアップ - 中安拓也のブログ

「Twitterでログイン」ボタンを作成する

続いてTwitterログイン用のボタンを作成していきます。TwitterのアイコンはFont AwesomeのTwitterアイコンを使用します。

f:id:l08084:20200419145552p:plain
「Twitterでログイン」ボタン

Angular Materialを使用しているので、<mat-icon class="sns-icon fab fa-twitter"></mat-icon>を使用することでTwiiterアイコンをボタンに埋め込むことができます。

下記のようにHTMLとSCSSを記載すればTwitterボタンが完成します。

  • login.component.html
      <button (click)="signInWithTwitter()" class="twitter" mat-raised-button>
        <mat-icon class="sns-icon fab fa-twitter"></mat-icon>Twitterでログイン
      </button>
  • login.component.scss
    .sns-icon {
      font-size: 20px;
      position: absolute;
      left: 15px;
      top: 25%;
    }

    .twitter {
      color: #FFF;
      background-color: #00acee;
      border-color: #00acee;
      font-weight: bold;
      width: 100%;
      margin-bottom: 10px;
    }

Twitter認証機能の実装

それではFirebaseと@angular/fireライブラリを使用して、Twitter認証機能の実装をやっていきます。@angular/fireライブラリのインストールが完了していない場合は、下記の記事を参考に実施してください。

AngularでFirebase認証(その1) Firebaseのセットアップ - 中安拓也のブログ

  • login.component.html
      <button (click)="signInWithTwitter()" class="twitter" mat-raised-button>
        <mat-icon class="sns-icon fab fa-twitter"></mat-icon>Twitterでログイン
      </button>
  • login.component.ts
import { Router } from '@angular/router';
import { AuthenticationService } from '../services/authentication.service';

export class LoginComponent implements OnInit {
  constructor(
    private router: Router,
    private authenticationService: AuthenticationService
  ) {}

  /**
   * Twitter認証でログイン
   *
   * @memberof LoginComponent
   */
  public async signInWithTwitter() {
    try {
      await this.authenticationService.signInWithTwitter();
      // ログインに成功したらホーム画面に遷移する
      this.router.navigate(['/home']);
    } catch (error) {
      console.log(error);
    }
  }

}
  • authentication.service.ts
import { AngularFireAuth } from '@angular/fire/auth';
import * as firebase from 'firebase/app';

export class AuthenticationService {
  constructor(public afAuth: AngularFireAuth) {}

  public signInWithTwitter(): Promise<auth.UserCredential> {
    return this.afAuth.auth.signInWithPopup(
      new firebase.auth.TwitterAuthProvider()
    );
  }
}

Twitterの認証APIを呼び出しているのは、下記の部分のコードとなります。

this.afAuth.auth.signInWithPopup(new firebase.auth.TwitterAuthProvider());

上記のソースコードを実装して「Twitterでログイン」ボタンを押下すると、下記のようにポップアップでTwitter認証のページが表示され、Twitter認証に成功するとログインが完了し、ホーム画面に遷移します。

f:id:l08084:20200419153728p:plain

補足: Twitter認証を利用したアカウント登録機能の実装

f:id:l08084:20200419154152p:plain
アカウント登録画面

Twitter認証を使用して、アカウント登録機能を実装する場合もTwitter認証でログインを実施する場合と全く同じ実装で実現できます(ログインと同様にthis.afAuth.auth.signInWithPopup(new firebase.auth.TwitterAuthProvider());を呼ぶことでアカウント登録も実装できる)。

というのも、Firebaseを使ったTwitter認証を実施した場合、Twitter認証を実施した時にそのユーザーが存在しなければ、Firebaseにアカウントが新規登録されるため、ログインもアカウント登録も同様のFirebase APIを呼び出すことで実現できるからです。

参考サイト

JavaScript による Twitter を使用した認証  |  Firebase

Build Firebase Login with Twitter in Angular 7|8|9 - Positronx.io

【2020年】TwitterのAPIに登録し、アクセスキー・トークンを取得する具体的な方法 | Rabbishar-ラビシャー

Angular7でFont AwesomeをAngular Materialに統合 - Qiita

Firebase を JavaScript プロジェクトに追加する

【IE11】PDFファイルを保存せずにそのまま開けるようにする

はじめに

SPAアプリ上でIE11ブラウザを使ってPDFファイルを参照する場合に、PDFファイルをダウンロードせずにそのまま開いて参照したいとの要望をクライアントからいただいたため、ソースコードを修正した。

環境

Angularを使用して作成されたSPAアプリ。利用ブラウザはIE11

  • Angular@6.1.2
  • typescript@2.8.4
  • Internet Explorer11

msSaveBlobからmsSaveOrOpenBlobに変更

修正内容だが、ファイルをローカルに保存する時に呼び出しているAPIをwindow.navigator.msSaveBlobからwindow.navigator.msSaveOrOpenBlob変更して、ポップアップに保存ボタンだけでなくファイルを開くボタンも表示されるようにした。

こうすることによってPDFファイルを保存してから閲覧するのではなくて、ファイルを保存せずに閲覧することもできるようにした。

// 修正前のコード
window.navigator.msSaveBlob(blob, fileName);

// 修正後のコード
window.navigator.msSaveOrOpenBlob(blob, fileName);

下記のように、msSaveBlobではなくmsSaveOrOpenBlobを使用することでポップアップにファイルを開くボタンを表示することができる。

f:id:l08084:20200406224401p:plain
msSaveBlobのポップアップ

f:id:l08084:20200406223933p:plain
msSaveOrOpenBlobのポップアップ

参考サイト

File API で作成した blob をダウンロードする | Hebikuzure's Tech Memo

IE11にはファイルをローカルに保存するJavaScriptのAPIが2種類用意されている。 - Qiita

msSaveBlob - Web APIs | MDN

msSaveOrOpenBlob - Web APIs | MDN

webpackで「JavaScript heap out of memory」エラー

はじめに

npm scriptsを使ってIonic(Angular/TypeScript)のAndroidビルドをしている時にFATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memoryエラーが発生して処理が落ちてしまった。

環境

  • Windows 10
  • Angular@5.0.1
  • ionic@3.9.5
  • webpack@3.8.1
  • node v8.11.3
  • npm@6.1.0

発生したエラー

発生しているエラーはヒープサイズ不足によるエラーとなるFATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

詳細なエラーメッセージは下記となる(省略部分あり)。

$ npm run build:android --env=dev4 --releasetype=android

<中略>

> webpack --progress --config ./config/webpack.config.js --hide-modules

69% building modules 1427/1437 modules 10 active ...odules\core-js\modules\_math-log1p 
69% building modules 1428/1437 modules 9 active ...odules\core-js\modules\_math-log1p. 
69% building modules 1429/1437 modules 8 active ...odules\core-js\modules\_math-log1p. 
69% building modules 1430/1437 modules 7 active ...odules\core-js\modules\_math-log1p. 

<中略>

91% additional asset processing       
<--- Last few GCs --->

[23324:000001DB86E99F80]  1136311 ms: Mark-sweep 1345.4 (1446.3) -> 1345.4 (1446.3) MB, 1025.9 / 0.0 ms  allocation failure GC in old space requested
[23324:000001DB86E99F80]  1137592 ms: Mark-sweep 1345.4 (1446.3) -> 1345.4 (1435.8) MB, 1154.1 / 0.0 ms  last resort GC in old space requested
[23324:000001DB86E99F80]  1138612 ms: Mark-sweep 1345.4 (1435.8) -> 1345.4 (1435.8) MB, 1019.8 / 0.0 ms  last resort GC in old space requested


<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 000003CA08025879 <JSObject>
    1: /* anonymous */(aka /* anonymous */) [000002FAFA6022D1 <undefined>:~3227] [pc=000001AC8582FFF7](this=000002FAFA6022D1 <undefined>,self=0000035515E06269 <AST_Array map 
= 000000C2D106EC59>,tw=00000317A7E51B79 <Compressor map = 000000C2D10709E9>)
    2: before [000002FAFA6022D1 <undefined>:~5479] [pc=000001AC8559B096](this=00000317A7E51B79 <Compressor map = 000000C2D10709E9>,node=000003551...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
 1: node_module_register
 2: v8::internal::FatalProcessOutOfMemory
 3: v8::internal::FatalProcessOutOfMemory
 4: v8::internal::Factory::NewCodeRaw
 5: v8::internal::Factory::NewCode
 6: v8::internal::modulo
 7: v8::internal::compiler::ControlFlowOptimizer::TryBuildSwitch
 8: v8::internal::interpreter::HandlerTableBuilder::HandlerTableBuilder
 9: v8::internal::compiler::Pipeline::AllocateRegistersForTesting
10: v8::internal::compiler::ValueNumberingReducer::operator=
11: v8::internal::CompilationJob::FinalizeJob
12: v8::internal::CompilationJob::isolate
13: v8::internal::Compiler::FinalizeCompilationJob
14: v8::internal::OptimizingCompileDispatcher::InstallOptimizedFunctions
15: v8::internal::StackGuard::HandleInterrupts
16: v8::internal::wasm::WasmOpcodes::TrapReasonMessage
17: 000001AC847043C1
npm ERR! code ELIFECYCLE
npm ERR! errno 3

<省略>

上記のエラーメッセージを見ていくとwebpackのコマンド(webpack --progress --config ./config/webpack.config.js --hide-modules)を実行しているタイミングでヒープサイズ不足のエラーが発生していることがわかる。

対処法

エラーが発生しているnpm scriptsのコマンド(build:ts)にオプションを追加して、--max_old_space_sizeのサイズを大きくしてから再実行すると直った。

  • package.json
// 修正前
"build:ts": "webpack --progress --config ./config/webpack.config.js --hide-modules"

// 修正後
"build:ts": "cross-env NODE_OPTIONS=--max_old_space_size=2048 webpack --progress --config ./config/webpack.config.js --hide-modules"

上記の通り、npm scriptsのwebpackのコマンドにcross-env NODE_OPTIONS=--max_old_space_size=2048を追記して、--max_old_space_sizeのサイズを拡大している。

なお、cross-envがインストールされていなくてエラーが発生する場合は、下記の通りcross-envをインストールする必要がある。

npm i cross-env

参考サイト

Webpack でビルドが稀に落ちる現象の回避 - Qiita

JavaScript heap out of memory が発生したときに試したこと ++ Gaji-Laboブログ

npm searchの「JavaScript heap out of memory」エラー対応 - Qiita

Angular Materialでレスポンシブなサイドメニューを実装する

はじめに

PCなどの大きいディスプレイの時はサイドメニューを常に表示し、モバイル端末などの小さいディスプレイの時はハンバーガーメニューに切り替えてスペースを節約したい。

f:id:l08084:20200320172927p:plain
画面が大きいときはサイドメニュー

f:id:l08084:20200320174008p:plain
画面の横幅が狭くなるとハンバーガーメニューになる

f:id:l08084:20200320174658p:plain
タップするとメニューが横から出てくる

このように端末のディスプレイサイズに合わせて表示形式が変わるサイドメニューを作成していく。

環境

本記事における実行環境と使用ライブラリについて

$ ng version

Angular CLI: 8.3.20
Node: 12.13.1
OS: darwin x64
Angular: 8.2.14
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.803.20
@angular-devkit/build-angular     0.803.20
@angular-devkit/build-optimizer   0.803.20
@angular-devkit/build-webpack     0.803.20
@angular-devkit/core              8.3.20
@angular-devkit/schematics        8.3.20
@angular/cdk                      7.3.7
@angular/cli                      8.3.20
@angular/fire                     5.2.3
@angular/material                 7.3.7
@ngtools/webpack                  8.3.20
@schematics/angular               8.3.20
@schematics/update                0.803.20
rxjs                              6.5.3
typescript                        3.5.3
webpack                           4.39.2

今回やること

レスポンシブなメニューを実装するために、今回やることだが下記となる。

  • ディスプレイサイズの検知
  • サイドメニューの表示・非表示の切り替え
  • ハンバーガーメニューの表示・非表示の切り替え

ダッシュボード画面

最初に画像で貼ったダッシュボード画面(home.component.html)の実装となる。

ポイントはヘッダー(<app-header>)とサイドメニュー(<mat-sidenav>)の実装、そしてディスプレイサイズを検知するisHandset$の3点である。

<!-- ヘッダー -->
<app-header [isHandset$]="isHandset$" (drawerToggled)="drawer.toggle()"></app-header>

<mat-sidenav-container class="container">
  <!-- サイドメニュー -->
  <mat-sidenav #drawer [attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
    [mode]="(isHandset$ | async) ? 'over' : 'side'" class="side-menu" [opened]="!(isHandset$ | async)">
    <app-main-side-menu [isHandset]="(isHandset$ | async)" (drawerClosed)="drawer.close()"></app-main-side-menu>
  </mat-sidenav>
  <!-- メイン コンテンツ -->
  <mat-sidenav-content class="main-contents">
    <!-- サブサイドメニュー -->
    <router-outlet></router-outlet>
  </mat-sidenav-content>
</mat-sidenav-container>

isHandset$

Angular CDKのBreakpointObserverを使用して画面が小さくなったらtrueを返す、isHandset$を定義する。

このisHandset$を使用してハンバーガーメニューを表示したりサイドメニューを消したりして、レスポンシブデザインを実現する。

  • home.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { map } from 'rxjs/operators';

// ...省略

export class HomeComponent {
  public isHandset$: Observable<boolean> = this.breakpointObserver
    .observe(Breakpoints.Handset)
    .pipe(map(result => result.matches));

  constructor(private breakpointObserver: BreakpointObserver) {}

}

ヘッダー(<app-header>)

ngIfisHandset$を使って画面が小さいときのみ、ハンバーガーメニューを表示している。isHandset$はObservableを返すので、asyncパイプも使用する必要がある。

  • header.component.html
<mat-toolbar color="primary">
  <div class="main">
    <!-- ハンバーガーメニュー -->
    <button type="button" mat-icon-button *ngIf="(isHandset$ | async)" (click)="toggle()">
      <mat-icon>menu</mat-icon>
    </button>

    <span class="logo" (click)="signOut()">
      <mat-icon class=" header-icon">
        note
      </mat-icon>
      <span class="title">3Memo</span>
    </span>
    <!-- ログアウトボタン(ログインしている時のみ表示される) -->
    <mat-icon matTooltip="ログアウトする" (click)="signOut()" *ngIf="authenticationService.getUser() | async"
      class="right-icon">exit_to_app
    </mat-icon>
  </div>
</mat-toolbar>

サイドメニュー(<mat-sidenav>)

サイドメニューの実装にはAngular MaterialのSidenavを使用している。

  <!-- サイドメニュー -->
  <mat-sidenav #drawer [attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
    [mode]="(isHandset$ | async) ? 'over' : 'side'" class="side-menu" [opened]="!(isHandset$ | async)">
    <!-- 省略... -->
  </mat-sidenav>

isHandset$を使用することでディスプレイが小さくなった時には、attr.roledialogにし、modeoverに、openedfalseにしている

参考サイト

Angular Material Responsive Navigation | by Ahmed Abouzied | Medium

https://material.angular.io/cdk/layout/overview

Angular Component Dev Kit 入門 - Qiita

Angular CDK Layoutを触ってみる - Qiita

【Angular Router】画面遷移でオブジェクトを渡す

はじめに

Angular Routerを使ってA画面からB画面に遷移する時に、B画面に単一の値ではなく、オブジェクトを渡したい時にどのような書き方をすればいいかまとめました。

環境

  • Angular CLI: 8.3.20
  • Node: 12.13.1
  • OS: darwin x64
  • Angular: 8.2.14

実装

メモのUpsert画面に画面遷移でオブジェクトを渡す処理を実装していきます。

URIの設定

まず、メモのUpsert画面のURI(/home/upsert)をAppRoutingModuleを作成することで設定します。

  • app-routing.module.ts
// ...省略

// メモのUpset画面のURIを設定する
const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent,
    canActivate: [AuthenticationGuard],
    children: [
      { path: '', redirectTo: 'upsert', pathMatch: 'full' },
      {
        path: 'upsert',
        component: MemoUpsertComponent,
        canActivate: [AuthenticationGuard]
      }
    ]
  }
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

オブジェクトを遷移先の画面に渡す

selectedMemoIdselectedFolderIdのプロパティーをもつオブジェクトparamを作成して、メモUpsert画面に遷移している処理です。なお、遷移先の画面にオブジェクトを渡しています。

    const param = {
      selectedMemoId: id,
      selectedFolderId: this.selectedFolderId
    };
    // メモUpsert画面に遷移する
    this.router.navigate(['/home/upsert', param]);

上記の処理を実施したときのURIは下記のようになります。

localhost:4200/home/upsert;selectedMemoId=9BE2uHckWqH;selectedFolderId=MqDAWFH7R4

オブジェクトとして渡している各プロパティーが名前=値の形式でセミコロン(;)区切りで渡されていることがわかります。このように、オブジェクトの値を渡すと、URIがクエリパラメーター表記(home/upsert/42)ではなく、マトリクスURI(Matrix URI)表記になります。

遷移先の画面でオブジェクトを受け取る

渡されたオブジェクトを遷移先の画面で受け取るには、下記のようにparams.getでオブジェクトのプロパティー名を指定してあげると遷移先のコンポーネント上でその値を使えるようになります。

  • memo-upsert.component.ts
// ...省略

    this.route.paramMap.subscribe((params: ParamMap) => {
      this.selectedMemoId = params.get('selectedMemoId');
      this.selectedFolderId = params.get('selectedFolderId');
    });
}

参考サイト

Angular 日本語ドキュメンテーション

AngularのngIf/elseとng-containerタグとng-templateタグを組み合わせて使う

毎回、AngularでngIf/elseを書くたびに書き方を忘れてググっているので書きました。

ngIf/elseとng-containerタグとng-templateタグを組み合わせて使う

ngIf/elseとng-containerタグとng-templateタグを組み合わせて使うことで、余計なタグを生成せずにDOMの表示を切り替えることができます。

ngIf-else構文

条件句の真偽値に応じて、表示するDOMを切り替えることができます。条件句が真の時はngIfのタグを、条件句が偽のときはelseで指定しているタグを表示します。

<ng-container>タグ

テンプレート上で使用しても何も表示されないタグです。*ngIf*ngForの構造化ディレクティブを付けて使用する事で余計なタグを生成せずに、表示するタグの切り替えや、ループを行うことができます。

<ng-container>
  <div>Hey!!!!</div>
</ng-container>

上記の例では、<ng-container>タグ自体はコメント化されるため表示されませんが、ラップしているdiv要素は表示されます

*ngFor<ng-container>上で使っている例

    <ng-container *ngFor="let fraction of fractionList; let i = index">
      <app-fraction-input [(fraction)]="fractionList[i]"></app-fraction-input>
    </ng-container>

<ng-template>タグ

デフォルトでは表示されずに、ラップしている要素ごとコメント化されるタグです。#で名前をつけることができます。

<ng-template>
  <div>Hey!!!!!!</div>
</ng-template>

上記の例では、<ng-template>タグ自体も<ng-template>タグにラップされているdiv要素も表示されません

組み合わせて使った例

ngIf-else構文により、条件句(memoId)がtrueのときは<ng-container>タグでラップされている要素が表示されます。また、条件句が偽の時にはelseで指定されているcreateLabelブロック(<ng-template>タグ)の方が表示されます。

<!-- 条件が真の時には「Update」と表示される -->
<ng-container *ngIf="memoId; else createLabel">Update</ng-container>
<!-- 条件が偽の時には「Create」と表示される -->
<ng-template #createLabel>Create</ng-template>

環境

  • Angular CLI: 8.3.20
  • Node: 12.13.1
  • OS: darwin x64
  • Angular: 8.2.14

参考サイト

コンポーネントにおけるObservableの購読 - Angular After Tutorial

Angularの便利タグng-container, ng-content, ng-template - Qiita

https://angular.jp/guide/structural-directives

【障害メモ】[Ionic v3][Android]電話の発信と管理を許可しないとDevice Idを取得できない

f:id:l08084:20200222203852p:plain
電話の発信と管理の許可についてのモーダル

障害内容

Ionic v3で作成されたAndroidアプリで、「電話の発信と管理を許可しますか?」と表示されるモーダルで「許可しない」を選択したユーザーの、端末識別ID(Unique Device ID)を取得できないエラーが発生した。

環境

  • cordova (Cordova CLI) : 8.0.0
  • Ionic Framework : ionic-angular 3.9.5
  • Android 9.0

原因

端末識別ID(Unique Device ID)を取得するのに使用しているCordovaプラグインであるUniqueDeviceIDが、電話の発信と管理を許可していないユーザーに対応していないのが原因となる。電話の発信と管理を許可していないユーザーの端末からDevice IDを取得できないため、下記のエラーを出力してDevice IDの取得に失敗する。

app.js:1 Exception occurred: getDeviceId: uid 10192 does not have android.permission.READ_PHONE_STATE.

エラーが発生した箇所

電話の発信と管理を許可していないユーザーの場合は、Device IDの取得に失敗するためthis.uniqueDeviceID.get()の部分で上記のエラーが発生する。

import { UniqueDeviceID } from '@ionic-native/unique-device-id';

@Injectable()
export class DeviceIdGetService {
    private deviceId: string;

    constructor(private uniqueDeviceID: UniqueDeviceID, private platform: Platform) {
        this.deviceId = '';
    }

    public init() {
        if (this.platform.is('cordova')) {
   // 電話の発信と管理を許可していない場合は、DeviceIdを取得できない
            this.uniqueDeviceID.get()
                .then((uuid: any) => {
                    this.deviceId = this.getPlatformPrefix().concat(uuid);
                })
                .catch((error: any) => {
                    console.log(error);
                });
        } else {
            this.deviceId = 'Browser';
        }
    }
}

対応方針

アプリによる電話の発信と管理を許可するようにユーザーに強制することはできないため、device idの取得に失敗してもアプリを利用できるようにするか、電話の発信と管理を許可していないユーザーを検知して、許可を促すメッセージを表示するしかない。