Create onboarding and its guard

Hello, I want to create a onboarding for my app. This is just a single page containing slides. Now this should be just shown, when the user enters the app the first time. For this, I thought I add an item ‘onboarded’ to the localStorage. Now the guard just needs to look if it gets a response when calling this property:

canActivate() {
    this.storageService.get(Constants.ONBOARDED).then( res => {
      if(!res) {
        return true;
      } else {
        return false;
      }
    })
  }

Now I have the problem, that not all paths return a value. Should I put the Prmoise in the onInit or is there a way to use a resolver to push the data to the guard before it is actually called?

Okay nvm, I think I found the solution:

canActivate(): Promise<boolean> {
    return new Promise(resolve => {
      this.storageService.get(Constants.ONBOARDED).then( res => {
        if(!res) {
          resolve(true);
        } else {
          resolve(false);
        }
      }).catch(err => {
        resolve(true);
      });
    });
  }

I don’t understand what “not all paths return a value” means, but don’t explicitly create Promises like this.

canActivate(): Promise<boolean> {
  return this.storageService.get(Constants.ONBOARDED)
    .then(onboarded => !onboarded);
}

If I use this, it works… If I use your version, it doesn’t work…

What does storageService look like?

import { Injectable } from '@angular/core';
import { Plugins } from '@capacitor/core'

const { Storage } = Plugins;

@Injectable({
  providedIn: 'root'
})
export class StorageService {

  constructor() { }

  async store(storageKey: string, value: any) {

    const encryptedValue = btoa(escape(JSON.stringify(value)));

    await Storage.set({
      key: storageKey,
      value: encryptedValue
    });

  }

  async get(storageKey: string) {

    const res = await Storage.get({ key: storageKey })

    if(res.value) {
      return JSON.parse(unescape(atob(res.value)));
    } else {
      return false;
    }
  }

  async remove(storageKey: string) {
    await Storage.remove({key: storageKey});
  }

  async clear() {
    await Storage.clear();
  }
}

I would just use Ionic Storage instead here, which would eliminate any need for all the manual JSON marshalling, use of the escape function that carries a big “don’t use me” warning, base64 encoding, or anything else that is throwing undesired exceptions.

I would make it part of StorageService's contract that it never throw exceptions.

So If I read the docs to Ionic Storage, I think this is a little ‘overkill’ for my simple Application:

Created for teams building complex, data-driven apps, apps managing sensitive data, …

And in this discussion we are at my main problem again ^^ I always have the problem that some people say I need to that and others say I should do that. For example, some say, I should use Promises whereever I can, others say, I should use Oservebales whenever possible (and then avoid Promises/Observables).
And here’s the same, I saw some tutorails using the system I have and now you tell me that it is crab… And I as a beginner sit here and can flip a coin who is right…

I think you’re referring to something called “Ionic Offline Storage”, with which I have no experience. I’m talking about this. It can use SQLite on device if you wish, or it can just be a frontend for IndexedDB, which works even in ordinary browser environments, making it super-convenient for development.

You have to decide a lot of things for yourself, and one of them is who to pay attention to. Personally, I treat official docs like those from Ionic, Angular, and MDN as the gold standard. There are bloggers (like Ben Lesh, for example) that I trust.

Basically, anything on the internet that tries to sell me “courses” I immediately blacklist. After a while, you develop your own eye for code quality.

I say that Promises are OK for things that are one-shot events, but generally I don’t deploy them in new code unless I’m dealing with an API that provides them. Converting a Promise to an Observable is really easy with the RxJS from operator. Anybody saying you should avoid Observables in a web app (especially an Angular one, where they’re woven deeply into the framework itself) is somebody I would ignore.

The only “tutorials” I would ever pay any attention to when I’m first starting out with a library are the official ones, like the Tour of Heroes. The variation in quality of J. Internet Rando “tutorials” is so great, and my ability to judge what is idiomatic and what isn’t for a framework or library is underdeveloped at that point, so I stick to getting a grounding with official docs.

2 Likes

I’m talking about this

Okay this seems to be quit easy in usage and does the same as LocalStorage. But still I have a question: Why should a custom StorageEngine be netter than the one provided by Capacitor, which is used to convert web apps into mobile apps?

To me the question would rather be “why did the Capacitor folks make yet another storage system that is harder to use than Ionic Storage, which predates the entire Capacitor project by years?”, and I don’t have a good answer for that.

I just implemented the IonicStorage. could you just have a quick look on my updated storage.service if everything in there is okay?

import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage'

@Injectable({
  providedIn: 'root'
})
export class StorageService {

  constructor(private storage: Storage) { }

  store(key: string, value: any) {
    this.storage.ready().then( () => {
      this.storage.set(key, value);
    });
  }

  get(key: string) {
    this.storage.ready().then(() => {
      this.storage.get(key).then(res => {
        if(!res) {
          return false;
        } else {
          return res;
        }
      })
    });
  }

  remove(key: string) {
    this.storage.ready().then(() => {
      this.storage.remove(key);
    });
  }
  
  clear() {
    this.storage.ready().then(() => {
      this.storage.clear();
    });
  }
}

EDIT:
Well, der must be a mistake, because When I use your version of a guard

canActivate(): Promise<boolean> {
    return this.storageService.get(Constants.ONBOARDED)
    .then(onboarded => !onboarded);   
  }

I get the error-message, that property then does not exist on type void

  • Declare types for function return values
  • Do absolutely everything in your power to avoid any
  • Read this post. In here lies your proximate problem - get is a “class C” function, and so it must be declared to return a Promise (or Observable), and its first word should be return. You have some design leeway for what store and remove are, but need to be aware of the consequences of the choices

So I would write get like this, which should give you an idea of how to fix the rest of StorageService:

get<T>(key: string): Promise<T> {
  return this.storage.ready().then(() => this.storage.get(key));
}

How do I avoid any if all items stored have a differnet structure?
What does <T> stand for?

Same answer to both questions.

get<T>(key: string): Promise<T> { return this.storage.ready().then(this.storage.get(key)); }
throws this error

 src/app/services/storage.service.ts:21:38
[ng]         21     return this.storage.ready().then(this.storage.get(key));
[ng]                                                 ~~~~~~~~~~~~~~~~~~~~~
[ng]         Did you forget to use 'await'?

Sorry, typo.

return this.storage.ready().then(() => this.storage.get(key));

I only need to add the <T> to store because there is a value, right? And should I return stuff from all methods and not only from get to catch errors?

store<T>(key: string, value: T) {
    this.storage.ready().then( () => {
      this.storage.set(key, value);
    });
  }

  get<T>(key: string): Promise<T> {
    return this.storage.ready().then(() => this.storage.get(key));
  }

  remove(key: string) {
    this.storage.ready().then(() => {
      this.storage.remove(key);
    });
  }
  
  clear() {
    this.storage.ready().then(() => {
      this.storage.clear();
    });
  }

I guess so. I would phrase it as improving type safety by avoiding any.

That depends on how you characterize them. In my three-type taxonomy, only the return values from type C functions are relevant. Type A and B functions should return void (as described in that post), because they are designed either so that (A) nobody cares when they finish or (B) they do care internally, but nobody outside needs to know.

This leads to another philosophical discussion about Storage. My position, as laid out there, is to write optimistically (so in my versions of these services you are writing, for example, set returns void).

I choose never to rely on Storage for in-app communication. You can do so, but if you do, then you must make set return a Promise, and you must religiously wait on every write you make before any code path may potentially read back, or you introduce a race condition whereby a read halfway across your app hits before your last write completed, thanks to one particular router change if the page has been cached on a SamBuzz K-350 phone that has just been rebooted, but only on Thursdays.

Obviously, that example is exaggerated, but race conditions do have a way of not showing up until your app is in production. I have been burned by so many I have lost count.

At first, I wanna thank you for your patience with me! For me this is all really new and it takes me some time to understand everything…

(A) nobody cares when they finish or (B) they do care internally, but nobody outside needs to know.

Since I try to build a little Game it is important to know, if the data sent to the Storage is really stored, which then means I need to declare all Methods to return a Promise and check it then.

And another thing. This <T> stands for declaring a type that is returned, right? I now understood how to use it in my storage-system but I don’t get how to use it in a resolver for example, that should return an array of json-objects:

export class NamenseingabeResolver implements Resolve<any> {

  constructor(private storageService: StorageService) { }

  resolve(): Promise<any> {
    return this.storageService.get(Constants.SPIELER)
    .then(res => {
      if(res)
        return res;
      else
        return [];
    });   
  }

}