Jul 24, 2020

Troubleshooting Custom Capacitor Plugin: No such module 'PushNotifications'

As a Capacitor hybrid app developer, default plugins offered by Capacitor can be not enough to achieve what we want for our project.

For beginner like me, who hope to create a simple custom plugin, set aside Javascript related code, it can be a tough experience as we have to learn and put into practise for both Android and iOS build.

Building a custom plugin for Capacitor (Ionic)

Fortunately, Capacitor has built-in API we could use to build any custom plugin.

No such module 'PushNotifications' (iOS)

On one of my custom plugin, it has dependency on Pusher's client iOS SDK which is imported through cocoapod. Running app "build" on xcode on this plugin is successful.

On my ionic app project folder, when import the created custom plugin causes error, and I get 
"No such module 'PushNotifications'
for importing the custom plugin. Which it's weird as I've checked and certain PushNotifications plugin installed in pods folder on both the actual app project and in the custom plugin.

When I navigate inside xcode Pods project, I'm able to see PushNotifications library correctly placed under pods folder.



Solution:

1. In the Capacitor custom plugin folder, find the .podspec file (usually located at root folder of your Plugin project), and add s.xcconfig to your podspec file, it's added to tell xcode about where to look the PushNotifications framework required by this plugin in your main project.
	
		
  require 'json'

  package = JSON.parse(File.read(File.join(__dir__, 'package.json')))

  Pod::Spec.new do |s|
    s.name = 'CapacitorPusherBeams'
    s.version = package['version']
    s.summary = package['description']
    s.license = package['license']
    s.homepage = package['repository']['url']
    s.author = package['author']
    s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
    s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
    s.ios.deployment_target  = '11.0'
    s.dependency 'Capacitor'
    s.swift_version = '5.0'
    s.xcconfig = { 'FRAMEWORK_SEARCH_PATHS' => "${PODS_CONFIGURATION_BUILD_DIR}/PushNotifications" }
  end

	

2. Or, alternatively, you can add the PushNotification framework path manually as below (you'll have to do this every time when the plugin get updated or after you run npx cap update)
Steps
  1. Go to Pods project's Build settings, find "Framework Search Paths"
  2. Select your affected plugin (by the missing module error mentioned above)
  3. Add the missing path to both Debug and Release as needed.

May 25, 2020

WebdriverIO - Appium, The "Error: Failed to create session."

Working on a hybrid app built with Ionic4, I've been writing tests with Angular's e2e, cucumber and selenium, now I'm surprised by variety of features & supported services offered by webdriverio.

I first went into the list of boilerplate projects and try to get my hand dirty with first sample project made for hybrid app.

I git cloned this repo - appium-boilerplate, updated capabilities values based on the simulator config available on my dev environment (you have to provide exact to the capabilities based on your AVD Manager - Android Studio).

Next, by running the command "npm run android.app", I'd expected appium magic would works instantly, but if as a first-timer-of-all-stuff, I ran into a little issue (but cost all day to find out the causes).


Troubleshooting

If you run into similar error message as following:

Error: Failed to create session.
read ECONNRESET    
  
    npm run android.app

    appium-boilerplate@5.3.1 android.app ~/appium-boilerplate
    wdio ./config/wdio.android.app.conf.js


    Execution of 6 spec files started at 2020-05-24T05:09:03.689Z

    [0-0] RUNNING in Android - /tests/specs/app.forms.spec.js
    [0-0]  Error:  Failed to create session.
    read ECONNRESET
    [0-0] FAILED in Android - /tests/specs/app.forms.spec.js
    [0-1] RUNNING in Android - /tests/specs/app.login.spec.js
    [0-1]  Error:  Failed to create session.
    read ECONNRESET
    [0-1] FAILED in Android - /tests/specs/app.login.spec.js
    [0-2] RUNNING in Android - /tests/specs/app.navigation.spec.js
    [0-2]  Error:  Failed to create session.
    read ECONNRESET
    [0-2] FAILED in Android - /tests/specs/app.navigation.spec.js
    [0-3] RUNNING in Android - /tests/specs/app.swipe.spec.js
    [0-3]  Error:  Failed to create session.
    read ECONNRESET
    [0-3] FAILED in Android - /tests/specs/app.swipe.spec.js
    [0-4] RUNNING in Android - /tests/specs/app.webview.spec.js
    [0-4]  Error:  Failed to create session.
    read ECONNRESET
    [0-4] FAILED in Android - /tests/specs/app.webview.spec.js
    [0-5] RUNNING in Android - /tests/specs/app.webview.xpath.spec.js
    [0-5]  Error:  Failed to create session.
    read ECONNRESET
    [0-5] FAILED in Android - /tests/specs/app.webview.xpath.spec.js

    Spec Files:  0 passed, 6 failed, 6 total (100% completed) in 00:02:07 

    npm ERR! code ELIFECYCLE
    npm ERR! errno 1
    npm ERR! appium-boilerplate@5.3.1 android.app: `wdio ./config/wdio.android.app.conf.js`
    npm ERR! Exit status 1
    npm ERR! 
    npm ERR! Failed at the appium-boilerplate@5.3.1 android.app script.
    npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

    npm ERR! A complete log of this run can be found in:
    npm ERR!     ~/.npm/_logs/2020-05-24T05_11_11_738Z-debug.log
  

Solution

I couldn't believe it (that's reason it costed my days to resolve it) until I tried it.
Start and run your virtual device (simulator) first before the webdriverio's command (npm run android.app)

Futhermore, you can always add another logPath as appium services' option, so you can look into the error log for troubleshooting.  

Oct 24, 2019

How to do Over The Air (OTA) distribution? (This tutorial is written for Adhoc Version only)

Why OTA Distribution?

You might have wondered is it possible to distribute your iOS app easily through your website.
If you're just distributing iOS app within your organization, you get to skip Apple's iOS review process which it got its fame by its strictness. With OTA you can just start publishing iOS app without the worries being rejected by App Review.

2 Types of iOS OTA Distribution

Each one require different type of Apple Developer enrolment:
  1. Adhoc Distribution - Apple Developer license
  2. Enterprise Program - Require high cost program (Apple Developer Enterprise Program)
Caveat: For 2nd point above, OTA distribution is meant for the purpose of distributing iOS app within one organization. Distributing the iOS app publicly is an act against EULA of Apple.

Differences between adhoc and enterprise OTA distribution:
  1. Adhoc OTA distribution only allow devices registered under the Provision Profile used by iOS build
  2. Enterprise distribution require your device to manually permit and "to trust" the distribution publisher.

In this article, I would only explain how to do the OTA under adhoc build, as I haven't got the opportunity to be enrolled into Apple Enterprise Program.
Even so, if you had Googling around, you would find out the distribution steps has similarity, the only different is the available choices during ipa generating.

What do you need for Adhoc OTA distribution?

Requirements:
  • Apple Developer Program (USD 99 annually)
  • iOS Distribution Certificate & releasing Profile (to be created in Apple Developer platform)

Steps to start generating iOS app for OTA distribution


Step 1: Change your device target to "Generic iOS Device" and start "Archive"

Archive

Step 2: Once the app is successfully archived

Successful Archive

Step 3: Select distribution method (choose Ad Hoc for small group distribution)


Note: Ad Hoc distribution allows devices registered under the attached to the releasing Profile to install the iOS build (ipa) only.
Ad Hoc - Distribution Method choices

Step 4:  Enable OTA installation

This step would enable add OTA installation by adding extra script (manifest.plist) later in the exported outcome. The plist file is required to allow Safari browser identify the app and communicate with your iOS device later.
Enable OTA installation

Enter hosting URL


Step 5: Signing App With Certificate and Profile

Automatic signing - xcode will try to find a suitable signing setting, it'll create a proper certificates/profiles into the Developer account
Manual - you use back known & existing setting (next screenshot)
Re-sign app



Step 6: Compile and Export

Compilation

Final Step: Exporting

Exported Files

Step 7: Final, minimal content required for manifest.plist for hosting

From step 6 above, manifest.plist & TSAwesomeProj.ipa are the only required files for simple OTA installation. You can find a lot of information in the original manifest.plist file,
Part of the content from manifest.plist file


but what's required is just as below. This is the content enough to get OTA installation working.
Simplified Version


Finally, only a simple HTML code is needed for OTA installation to happen in Safari browser. You just need to add "href" attribute value below for hyperlink.
itms-services://?action=download-manifest&url=URL_TO_MANIFEST_FILE
Example: 
<a href="itms-services://?action=download-manifest&url=https://test.com/manifest.plist">TSAwesomeProj Sample</a>

Aug 21, 2019

Angular: What does "Cannot read property 'childNodes' of null" trying to tell?

Recently, I ran into the following error, it has no obvious problem at first until part of my component template didn't show a thing until a click event is triggered the at the screen.

Cannot read property 'childNodes' of null

The error do not directly indicate which component or code in a template has gone wrong, but you know it's an error about certain part of the code is referring to "childNodes" on a null object.

Finding out the root cause

To find out where in the template has gone wrong, I didn't suspect it's problem with HTML template. I had started with checking my component class code, and though some binding values weren't pass correctly into the template code (the template code look fine, not bad syntax at all!).

Until I gave up trying consciously, I blindly test by removing of the HTML code part by part. This is where it shed me the ray of hope, eventually, I found out it's ion-card Ionic's component doesn't accept [innerHtml] correctly.

If you insist on using ion-card, you need a proper "child node" place inside it (so now I learnt, "child node" means the child element of a parent node "ion-card".), for ion-card, one of the options is ion-card-content.
I changed my code from:

<ion-card [innerHtml]="content" padding></ion-card>

to

<ion-card>
  <ion-card-content [innerHtml]="content"></ion-card-content>
</ion-card>

Separating [innerHtml] from ion-card would solve the childNode problem above. You might have different parent node causing the null childNodes issue too. So to prevent further time wasting effort finding the root-cause and if you run into similar issue, just keep in mind, if you get similar error, it means there is problem in your template/html code.

Aug 14, 2019

TypeError: Found non-callable @@iterator - Migration of Angular 7 to 8 (upgrading Ionic: 4.6.0 > 4.7.1)

Troubleshoot migration problems

TypeError: Found non-callable @@iterator

TypeError: Found non-callable @@iterator
The non-descriptive error message Found non-callable @@iterator, for new Angular 8 user like me, has made the upgrading journey a difficult experience.

And my finding, the cause of the problem above is due to the way interceptor written in Angular 7.

Causes: HttpRequest's headers "set"

I noticed the use of request.headers.set() causing the issue above. My sample code (causes):

intercept(req: HttpRequest, next: HttpHandler): Observable> {
  let headers = req.headers;
  const sampleToken = 'SAMPLE_TOKEN';
  headers = req.headers.set('Authorization', sampleToken);

  return next.handle(req.clone({ headers }));
}


After multiple trial and error, the only way to resolve is avoid using the same way to add header with such solution.

Solution: Headers modification

- on one single header modification, do the following

const yourToken = 'YOUR_TOKEN';
const apiReq = req.clone({
  headers: new HttpHeaders({'Authorization', yourToken})
});
return next.handle(apiReq);


- on multiple headers modification, do the following

const headers = { Authorization: authToken };
const apiReq = req.clone({
  setHeaders: headers
});
return next.handle(apiReq);
P/S: header value must be in string value