capacitor-google-tag-manager 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'CapacitorGoogleTagManager'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = package['license']
10
+ s.homepage = package['repository']['url']
11
+ s.author = package['author']
12
+ s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
+ s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
14
+ s.public_header_files = 'ios/Sources/GoogleTagManagerLinkage/include/*.h'
15
+ s.ios.deployment_target = '15.0'
16
+ s.dependency 'Capacitor'
17
+ # Any 9.x from 9.1.0, as in Package.swift. An app pins another one in its Podfile: pod 'GoogleTagManager', '9.3.0'
18
+ s.dependency 'GoogleTagManager', '~> 9.1'
19
+ s.swift_version = '5.9'
20
+ end
package/Package.swift ADDED
@@ -0,0 +1,41 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ // The plugin's job is the dependency on Google's Tag Manager package: an app that installs the plugin gets the SDK linked
5
+ // in, with the Firebase Analytics it depends on. Any 9.x is accepted; the app's Package.resolved records which one it
6
+ // builds with, and the app moves it (Xcode's File > Packages > Update to Latest Package Versions, or an edit of
7
+ // Package.resolved) without a release of this plugin.
8
+ let package = Package(
9
+ name: "CapacitorGoogleTagManager",
10
+ platforms: [.iOS(.v15)],
11
+ products: [
12
+ .library(
13
+ name: "CapacitorGoogleTagManager",
14
+ targets: ["GoogleTagManagerPlugin"])
15
+ ],
16
+ dependencies: [
17
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
18
+ .package(url: "https://github.com/googleanalytics/google-tag-manager-ios-sdk.git", "9.1.0"..<"10.0.0")
19
+ ],
20
+ targets: [
21
+ // References the SDK's classes so the linker takes its static archive into the app (see the .m file).
22
+ .target(
23
+ name: "GoogleTagManagerLinkage",
24
+ dependencies: [
25
+ .product(name: "GoogleTagManager", package: "google-tag-manager-ios-sdk")
26
+ ],
27
+ path: "ios/Sources/GoogleTagManagerLinkage"),
28
+ .target(
29
+ name: "GoogleTagManagerPlugin",
30
+ dependencies: [
31
+ "GoogleTagManagerLinkage",
32
+ .product(name: "Capacitor", package: "capacitor-swift-pm"),
33
+ .product(name: "Cordova", package: "capacitor-swift-pm")
34
+ ],
35
+ path: "ios/Sources/GoogleTagManagerPlugin"),
36
+ .testTarget(
37
+ name: "GoogleTagManagerPluginTests",
38
+ dependencies: ["GoogleTagManagerPlugin"],
39
+ path: "ios/Tests/GoogleTagManagerPluginTests")
40
+ ]
41
+ )
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # capacitor-google-tag-manager
2
+
3
+ Brings the [Google Tag Manager](https://developers.google.com/tag-platform/tag-manager) SDK into a Capacitor 8 app, on
4
+ Android and iOS, so that a Tag Manager container the app ships is loaded and acts on the app's Firebase Analytics events.
5
+
6
+ Tag Manager for Firebase has no API to call: once the SDK is linked into the app it finds the container and the Analytics
7
+ events by itself. What it needs from a Capacitor app is to be in the native build, and that is all this plugin does. The
8
+ one method it adds, `isAvailable`, tells from the web view whether the SDK made it into the build, because a build without
9
+ it runs, ships its container and says nothing.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install capacitor-google-tag-manager
15
+ npx cap sync
16
+ ```
17
+
18
+ Requires Capacitor 8, Android 8.0 (API 26) or newer and iOS 15 or newer. The SDK depends on Firebase Analytics, so the app
19
+ needs Firebase set up as usual (for example with `@capacitor-firebase/analytics`); this plugin does not configure it.
20
+
21
+ | Platform | What the plugin adds |
22
+ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | Android | `com.google.android.gms:play-services-tagmanager`, 18.3.0 by default. The SDK's own manifest declares `TagManagerPreviewActivity` with the `tagmanager.c.<application id>` scheme. |
24
+ | iOS | Google's `google-tag-manager-ios-sdk` Swift package, any 9.x from 9.1.0, or the `GoogleTagManager` pod `~> 9.1` with CocoaPods; Firebase Analytics comes with either. |
25
+ | Web | Nothing. A web build that wants Tag Manager loads gtm.js itself; `isAvailable` answers `false`. |
26
+
27
+ ### What stays in the app
28
+
29
+ The plugin brings the SDK and nothing that belongs to one app:
30
+
31
+ - **The container.** Export it from Tag Manager and put it where the SDK looks:
32
+ - Android: `android/app/src/main/assets/containers/GTM-XXXXXXX.json`.
33
+ - iOS: a folder reference named `container` in the app target's resources, holding `GTM-XXXXXXX.json`.
34
+ - **The iOS preview scheme.** Tag Manager's preview opens the app through `tagmanager.c.<bundle id>`, which iOS learns
35
+ only from the app's `Info.plist`:
36
+
37
+ ```xml
38
+ <key>CFBundleURLTypes</key>
39
+ <array>
40
+ <dict>
41
+ <key>CFBundleURLName</key>
42
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
43
+ <key>CFBundleURLSchemes</key>
44
+ <array>
45
+ <string>tagmanager.c.$(PRODUCT_BUNDLE_IDENTIFIER)</string>
46
+ </array>
47
+ </dict>
48
+ </array>
49
+ ```
50
+
51
+ On Android nothing is needed: the manifest merge puts the application id into the SDK's own preview activity.
52
+
53
+ ### Choosing the SDK version
54
+
55
+ - **Android:** set `playServicesTagManagerVersion` in the app's `android/variables.gradle`:
56
+
57
+ ```groovy
58
+ ext {
59
+ playServicesTagManagerVersion = '18.3.0'
60
+ }
61
+ ```
62
+
63
+ - **iOS, Swift Package Manager:** the plugin accepts any 9.x from 9.1.0, and the version the app builds with is the one in
64
+ its `Package.resolved` (`ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved`). Commit that
65
+ file, and move the version with Xcode's File > Packages > Update to Latest Package Versions, or by editing the pin and
66
+ resolving again (`xcodebuild -resolvePackageDependencies`). `npx cap sync` regenerates the app's `CapApp-SPM` package but
67
+ leaves `Package.resolved` alone, so the pin survives a sync.
68
+ - **iOS, CocoaPods:** pin it in the `Podfile`: `pod 'GoogleTagManager', '9.1.0'`.
69
+
70
+ The app should not declare the SDK itself as well: the plugin's declaration is the only one needed.
71
+
72
+ ### Why declaring the iOS SDK is not enough
73
+
74
+ Google ships the iOS SDK as a static archive, and an app never calls it: `TAGManager` starts itself from `+load`. A linker
75
+ takes a member of a static archive only when something references a symbol it defines, and Swift Package Manager links
76
+ without `-ObjC`, so an app that merely depends on the package gets the SDK's (empty) framework folder in its bundle and
77
+ none of its code. The plugin's `GoogleTagManagerLinkage` target references every class the SDK exports, which makes the
78
+ linker take them; `isAvailable` then looks each of them up in the running app. CocoaPods links with `-ObjC` and is
79
+ unaffected either way.
80
+
81
+ ## Use
82
+
83
+ ```typescript
84
+ import { GoogleTagManager } from 'capacitor-google-tag-manager';
85
+
86
+ const { available } = await GoogleTagManager.isAvailable();
87
+ ```
88
+
89
+ ## Development
90
+
91
+ - `npm test` runs the TypeScript unit tests, `npm run test:parity` checks that both platforms implement exactly the API.
92
+ - `npm run test:android` runs the JVM tests, which also find the SDK on the plugin's class path.
93
+ - `npm run test:ios` runs the XCTest target on an iOS simulator, which also finds the SDK in the test binary.
94
+ - `npm run example:android:build` and `npm run example:ios:build` build `example-app/`, a page that calls `isAvailable`, the
95
+ way an app installs the plugin.
96
+
97
+ ## API
98
+
99
+ <docgen-index>
100
+
101
+ * [`isAvailable()`](#isavailable)
102
+ * [Interfaces](#interfaces)
103
+
104
+ </docgen-index>
105
+
106
+ <docgen-api>
107
+ <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
108
+
109
+ The plugin's job is to bring the Google Tag Manager SDK into the app's native build. The SDK then loads the container the
110
+ app ships and acts on Firebase Analytics events by itself; it has no JavaScript API, and neither does this plugin beyond
111
+ the one check below.
112
+
113
+ ### isAvailable()
114
+
115
+ ```typescript
116
+ isAvailable() => Promise<AvailabilityResult>
117
+ ```
118
+
119
+ Whether the Tag Manager SDK made it into the running app.
120
+
121
+ A build that lost the SDK still runs, ships its container and logs nothing: the container is simply never loaded. This
122
+ is the check that tells such a build apart from a healthy one, from the web view.
123
+
124
+ **Returns:** <code>Promise&lt;<a href="#availabilityresult">AvailabilityResult</a>&gt;</code>
125
+
126
+ **Since:** 0.1.0
127
+
128
+ --------------------
129
+
130
+
131
+ ### Interfaces
132
+
133
+
134
+ #### AvailabilityResult
135
+
136
+ What `isAvailable` answers.
137
+
138
+ | Prop | Type | Description |
139
+ | --------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
140
+ | **`available`** | <code>boolean</code> | `true` when the Google Tag Manager SDK's classes are in the running app: `TAGManager` on iOS, `com.google.android.gms.tagmanager.TagManagerService` on Android. Always `false` on the web, which has no native SDK. |
141
+
142
+ </docgen-api>
@@ -0,0 +1,64 @@
1
+ ext {
2
+ junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
+ // The Tag Manager SDK this plugin brings. An app that needs another version sets playServicesTagManagerVersion in its
4
+ // variables.gradle, the way it sets the Capacitor versions; Gradle resolves one version for the whole app either way.
5
+ playServicesTagManagerVersion = project.hasProperty('playServicesTagManagerVersion') ? rootProject.ext.playServicesTagManagerVersion : '18.3.0'
6
+ }
7
+
8
+ buildscript {
9
+ ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '2.2.20'
10
+ repositories {
11
+ google()
12
+ mavenCentral()
13
+ }
14
+ dependencies {
15
+ classpath 'com.android.tools.build:gradle:9.5.1'
16
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
17
+ }
18
+ }
19
+
20
+ apply plugin: 'com.android.library'
21
+ apply plugin: 'kotlin-android'
22
+
23
+ android {
24
+ namespace = "io.github.maximbelov.googletagmanager"
25
+ compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
26
+ defaultConfig {
27
+ minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 26
28
+ targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
29
+ versionCode 1
30
+ versionName "1.0"
31
+ }
32
+ buildTypes {
33
+ release {
34
+ minifyEnabled false
35
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
36
+ }
37
+ }
38
+ lintOptions {
39
+ abortOnError = false
40
+ }
41
+ compileOptions {
42
+ sourceCompatibility JavaVersion.VERSION_21
43
+ targetCompatibility JavaVersion.VERSION_21
44
+ }
45
+ }
46
+
47
+ kotlin {
48
+ jvmToolchain(21)
49
+ }
50
+
51
+ repositories {
52
+ google()
53
+ mavenCentral()
54
+ }
55
+
56
+ dependencies {
57
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
58
+ implementation project(':capacitor-android')
59
+ // The whole point of the plugin. Its manifest (play-services-tagmanager-api) declares TagManagerService and
60
+ // TagManagerPreviewActivity, the latter with the tagmanager.c.${applicationId} scheme, which the app's manifest merge
61
+ // resolves to the app's own id, so the plugin's manifest repeats neither.
62
+ implementation "com.google.android.gms:play-services-tagmanager:$playServicesTagManagerVersion"
63
+ testImplementation "junit:junit:$junitVersion"
64
+ }
@@ -0,0 +1,8 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!--
3
+ Nothing to declare here. The Tag Manager SDK's own manifest (play-services-tagmanager-api) brings TagManagerService,
4
+ the INTERNET and ACCESS_NETWORK_STATE permissions, and TagManagerPreviewActivity with its
5
+ tagmanager.c.${applicationId} scheme, and the app's manifest merge puts the app's id into that scheme. A second
6
+ declaration of the activity here would only be merged into the same element.
7
+ -->
8
+ </manifest>
@@ -0,0 +1,34 @@
1
+ package io.github.maximbelov.googletagmanager
2
+
3
+ import com.getcapacitor.JSObject
4
+ import com.getcapacitor.Plugin
5
+ import com.getcapacitor.PluginCall
6
+ import com.getcapacitor.PluginMethod
7
+ import com.getcapacitor.annotation.CapacitorPlugin
8
+
9
+ /**
10
+ * Google Tag Manager on Android. The plugin exists to put play-services-tagmanager into the app: the SDK finds the
11
+ * container in assets/containers and the Firebase Analytics events on its own, and its manifest brings the preview
12
+ * activity. What is left for the web view is the one question of whether the SDK is there.
13
+ */
14
+ @CapacitorPlugin(name = "GoogleTagManager")
15
+ class GoogleTagManagerPlugin : Plugin() {
16
+ @PluginMethod
17
+ fun isAvailable(call: PluginCall) {
18
+ call.resolve(JSObject().put("available", TagManagerSdk.isLinked(javaClass.classLoader)))
19
+ }
20
+ }
21
+
22
+ /** Whether the Tag Manager SDK's classes are on the app's class path. */
23
+ object TagManagerSdk {
24
+ /** A class the SDK has had since it moved to Google Play services, and the one its manifest names as a service. */
25
+ const val SERVICE_CLASS = "com.google.android.gms.tagmanager.TagManagerService"
26
+
27
+ fun isLinked(loader: ClassLoader?, className: String = SERVICE_CLASS): Boolean =
28
+ try {
29
+ Class.forName(className, false, loader)
30
+ true
31
+ } catch (_: ClassNotFoundException) {
32
+ false
33
+ }
34
+ }
File without changes
package/dist/docs.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "api": {
3
+ "name": "GoogleTagManagerPlugin",
4
+ "slug": "googletagmanagerplugin",
5
+ "docs": "The plugin's job is to bring the Google Tag Manager SDK into the app's native build. The SDK then loads the container the\napp ships and acts on Firebase Analytics events by itself; it has no JavaScript API, and neither does this plugin beyond\nthe one check below.",
6
+ "tags": [],
7
+ "methods": [
8
+ {
9
+ "name": "isAvailable",
10
+ "signature": "() => Promise<AvailabilityResult>",
11
+ "parameters": [],
12
+ "returns": "Promise<AvailabilityResult>",
13
+ "tags": [
14
+ {
15
+ "name": "since",
16
+ "text": "0.1.0"
17
+ }
18
+ ],
19
+ "docs": "Whether the Tag Manager SDK made it into the running app.\n\nA build that lost the SDK still runs, ships its container and logs nothing: the container is simply never loaded. This\nis the check that tells such a build apart from a healthy one, from the web view.",
20
+ "complexTypes": [
21
+ "AvailabilityResult"
22
+ ],
23
+ "slug": "isavailable"
24
+ }
25
+ ],
26
+ "properties": []
27
+ },
28
+ "interfaces": [
29
+ {
30
+ "name": "AvailabilityResult",
31
+ "slug": "availabilityresult",
32
+ "docs": "What `isAvailable` answers.",
33
+ "tags": [],
34
+ "methods": [],
35
+ "properties": [
36
+ {
37
+ "name": "available",
38
+ "tags": [],
39
+ "docs": "`true` when the Google Tag Manager SDK's classes are in the running app: `TAGManager` on iOS,\n`com.google.android.gms.tagmanager.TagManagerService` on Android. Always `false` on the web, which has no native SDK.",
40
+ "complexTypes": [],
41
+ "type": "boolean"
42
+ }
43
+ ]
44
+ }
45
+ ],
46
+ "enums": [],
47
+ "typeAliases": [],
48
+ "pluginConfigs": []
49
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What `isAvailable` answers.
3
+ */
4
+ export interface AvailabilityResult {
5
+ /**
6
+ * `true` when the Google Tag Manager SDK's classes are in the running app: `TAGManager` on iOS,
7
+ * `com.google.android.gms.tagmanager.TagManagerService` on Android. Always `false` on the web, which has no native SDK.
8
+ */
9
+ available: boolean;
10
+ }
11
+ /**
12
+ * The plugin's job is to bring the Google Tag Manager SDK into the app's native build. The SDK then loads the container the
13
+ * app ships and acts on Firebase Analytics events by itself; it has no JavaScript API, and neither does this plugin beyond
14
+ * the one check below.
15
+ */
16
+ export interface GoogleTagManagerPlugin {
17
+ /**
18
+ * Whether the Tag Manager SDK made it into the running app.
19
+ *
20
+ * A build that lost the SDK still runs, ships its container and logs nothing: the container is simply never loaded. This
21
+ * is the check that tells such a build apart from a healthy one, from the web view.
22
+ *
23
+ * @since 0.1.0
24
+ */
25
+ isAvailable(): Promise<AvailabilityResult>;
26
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=definitions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * What `isAvailable` answers.\n */\nexport interface AvailabilityResult {\n /**\n * `true` when the Google Tag Manager SDK's classes are in the running app: `TAGManager` on iOS,\n * `com.google.android.gms.tagmanager.TagManagerService` on Android. Always `false` on the web, which has no native SDK.\n */\n available: boolean;\n}\n\n/**\n * The plugin's job is to bring the Google Tag Manager SDK into the app's native build. The SDK then loads the container the\n * app ships and acts on Firebase Analytics events by itself; it has no JavaScript API, and neither does this plugin beyond\n * the one check below.\n */\nexport interface GoogleTagManagerPlugin {\n /**\n * Whether the Tag Manager SDK made it into the running app.\n *\n * A build that lost the SDK still runs, ships its container and logs nothing: the container is simply never loaded. This\n * is the check that tells such a build apart from a healthy one, from the web view.\n *\n * @since 0.1.0\n */\n isAvailable(): Promise<AvailabilityResult>;\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import type { GoogleTagManagerPlugin } from './definitions';
2
+ declare const GoogleTagManager: GoogleTagManagerPlugin;
3
+ export * from './definitions';
4
+ export { GoogleTagManager };
@@ -0,0 +1,7 @@
1
+ import { registerPlugin } from '@capacitor/core';
2
+ const GoogleTagManager = registerPlugin('GoogleTagManager', {
3
+ web: () => import('./web').then((m) => new m.GoogleTagManagerWeb()),
4
+ });
5
+ export * from './definitions';
6
+ export { GoogleTagManager };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,gBAAgB,GAAG,cAAc,CAAyB,kBAAkB,EAAE;IAClF,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,mBAAmB,EAAE,CAAC;CACpE,CAAC,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,CAAC","sourcesContent":["import { registerPlugin } from '@capacitor/core';\n\nimport type { GoogleTagManagerPlugin } from './definitions';\n\nconst GoogleTagManager = registerPlugin<GoogleTagManagerPlugin>('GoogleTagManager', {\n web: () => import('./web').then((m) => new m.GoogleTagManagerWeb()),\n});\n\nexport * from './definitions';\nexport { GoogleTagManager };\n"]}
@@ -0,0 +1,9 @@
1
+ import { WebPlugin } from '@capacitor/core';
2
+ import type { AvailabilityResult, GoogleTagManagerPlugin } from './definitions';
3
+ /**
4
+ * A browser has no native Tag Manager SDK, so the answer is `false` rather than an error: the question has an answer here.
5
+ * A web build that wants Tag Manager loads gtm.js itself.
6
+ */
7
+ export declare class GoogleTagManagerWeb extends WebPlugin implements GoogleTagManagerPlugin {
8
+ isAvailable(): Promise<AvailabilityResult>;
9
+ }
@@ -0,0 +1,11 @@
1
+ import { WebPlugin } from '@capacitor/core';
2
+ /**
3
+ * A browser has no native Tag Manager SDK, so the answer is `false` rather than an error: the question has an answer here.
4
+ * A web build that wants Tag Manager loads gtm.js itself.
5
+ */
6
+ export class GoogleTagManagerWeb extends WebPlugin {
7
+ async isAvailable() {
8
+ return { available: false };
9
+ }
10
+ }
11
+ //# sourceMappingURL=web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C;;;GAGG;AACH,MAAM,OAAO,mBAAoB,SAAQ,SAAS;IAChD,KAAK,CAAC,WAAW;QACf,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type { AvailabilityResult, GoogleTagManagerPlugin } from './definitions';\n\n/**\n * A browser has no native Tag Manager SDK, so the answer is `false` rather than an error: the question has an answer here.\n * A web build that wants Tag Manager loads gtm.js itself.\n */\nexport class GoogleTagManagerWeb extends WebPlugin implements GoogleTagManagerPlugin {\n async isAvailable(): Promise<AvailabilityResult> {\n return { available: false };\n }\n}\n"]}
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ var core = require('@capacitor/core');
4
+
5
+ const GoogleTagManager = core.registerPlugin('GoogleTagManager', {
6
+ web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.GoogleTagManagerWeb()),
7
+ });
8
+
9
+ /**
10
+ * A browser has no native Tag Manager SDK, so the answer is `false` rather than an error: the question has an answer here.
11
+ * A web build that wants Tag Manager loads gtm.js itself.
12
+ */
13
+ class GoogleTagManagerWeb extends core.WebPlugin {
14
+ async isAvailable() {
15
+ return { available: false };
16
+ }
17
+ }
18
+
19
+ var web = /*#__PURE__*/Object.freeze({
20
+ __proto__: null,
21
+ GoogleTagManagerWeb: GoogleTagManagerWeb
22
+ });
23
+
24
+ exports.GoogleTagManager = GoogleTagManager;
25
+ //# sourceMappingURL=plugin.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst GoogleTagManager = registerPlugin('GoogleTagManager', {\n web: () => import('./web').then((m) => new m.GoogleTagManagerWeb()),\n});\nexport * from './definitions';\nexport { GoogleTagManager };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\n/**\n * A browser has no native Tag Manager SDK, so the answer is `false` rather than an error: the question has an answer here.\n * A web build that wants Tag Manager loads gtm.js itself.\n */\nexport class GoogleTagManagerWeb extends WebPlugin {\n async isAvailable() {\n return { available: false };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,gBAAgB,GAAGA,mBAAc,CAAC,kBAAkB,EAAE;AAC5D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,mBAAmB,EAAE,CAAC;AACvE,CAAC;;ACFD;AACA;AACA;AACA;AACO,MAAM,mBAAmB,SAASC,cAAS,CAAC;AACnD,IAAI,MAAM,WAAW,GAAG;AACxB,QAAQ,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE;AACnC,IAAI;AACJ;;;;;;;"}
package/dist/plugin.js ADDED
@@ -0,0 +1,28 @@
1
+ var capacitorGoogleTagManager = (function (exports, core) {
2
+ 'use strict';
3
+
4
+ const GoogleTagManager = core.registerPlugin('GoogleTagManager', {
5
+ web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.GoogleTagManagerWeb()),
6
+ });
7
+
8
+ /**
9
+ * A browser has no native Tag Manager SDK, so the answer is `false` rather than an error: the question has an answer here.
10
+ * A web build that wants Tag Manager loads gtm.js itself.
11
+ */
12
+ class GoogleTagManagerWeb extends core.WebPlugin {
13
+ async isAvailable() {
14
+ return { available: false };
15
+ }
16
+ }
17
+
18
+ var web = /*#__PURE__*/Object.freeze({
19
+ __proto__: null,
20
+ GoogleTagManagerWeb: GoogleTagManagerWeb
21
+ });
22
+
23
+ exports.GoogleTagManager = GoogleTagManager;
24
+
25
+ return exports;
26
+
27
+ })({}, capacitorExports);
28
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst GoogleTagManager = registerPlugin('GoogleTagManager', {\n web: () => import('./web').then((m) => new m.GoogleTagManagerWeb()),\n});\nexport * from './definitions';\nexport { GoogleTagManager };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\n/**\n * A browser has no native Tag Manager SDK, so the answer is `false` rather than an error: the question has an answer here.\n * A web build that wants Tag Manager loads gtm.js itself.\n */\nexport class GoogleTagManagerWeb extends WebPlugin {\n async isAvailable() {\n return { available: false };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,gBAAgB,GAAGA,mBAAc,CAAC,kBAAkB,EAAE;IAC5D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,mBAAmB,EAAE,CAAC;IACvE,CAAC;;ICFD;IACA;IACA;IACA;IACO,MAAM,mBAAmB,SAASC,cAAS,CAAC;IACnD,IAAI,MAAM,WAAW,GAAG;IACxB,QAAQ,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE;IACnC,IAAI;IACJ;;;;;;;;;;;"}
@@ -0,0 +1,49 @@
1
+ #import "GoogleTagManagerLinkage.h"
2
+
3
+ // The Tag Manager SDK ships as a static archive, and nothing in an app calls it: TAGManager starts itself from +load and
4
+ // then follows Firebase Analytics. A linker takes an archive member only when something references a symbol it defines,
5
+ // and an app linked without -ObjC (Swift Package Manager adds none) therefore takes none of it. The archive's framework
6
+ // directory still lands in the bundle, so the build looks complete while the SDK, and with it the container, is absent.
7
+ //
8
+ // So every class the archive exports is referenced here. The SDK publishes no headers for them, which is why they are
9
+ // declared below, bare: the declarations only give the compiler a name, and each `[X class]` leaves an undefined
10
+ // `_OBJC_CLASS_$_X` that the linker must resolve from the archive. CocoaPods links with -ObjC anyway; this costs it nothing.
11
+ @interface TAGContainer : NSObject
12
+ @end
13
+ @interface TAGContainerLoader : NSObject
14
+ @end
15
+ @interface TAGDispatchScheduler : NSObject
16
+ @end
17
+ @interface TAGHit : NSObject
18
+ @end
19
+ @interface TAGHitSender : NSObject
20
+ @end
21
+ @interface TAGHitStore : NSObject
22
+ @end
23
+ @interface TAGJSExportedInstructions : NSObject
24
+ @end
25
+ @interface TAGLogger : NSObject
26
+ @end
27
+ @interface TAGManager : NSObject
28
+ @end
29
+ @interface TAGNetReachability : NSObject
30
+ @end
31
+ @interface TAGPendingEvent : NSObject
32
+ @end
33
+ @interface TAGRuntime : NSObject
34
+ @end
35
+ @interface TAGUniversalAnalytics : NSObject
36
+ @end
37
+
38
+ NSArray<NSString *> *GoogleTagManagerLinkedClassNames(void) {
39
+ NSArray<Class> *classes = @[
40
+ [TAGContainer class], [TAGContainerLoader class], [TAGDispatchScheduler class], [TAGHit class], [TAGHitSender class],
41
+ [TAGHitStore class], [TAGJSExportedInstructions class], [TAGLogger class], [TAGManager class],
42
+ [TAGNetReachability class], [TAGPendingEvent class], [TAGRuntime class], [TAGUniversalAnalytics class]
43
+ ];
44
+ NSMutableArray<NSString *> *names = [NSMutableArray arrayWithCapacity:classes.count];
45
+ for (Class cls in classes) {
46
+ [names addObject:NSStringFromClass(cls)];
47
+ }
48
+ return names;
49
+ }
@@ -0,0 +1,8 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /// The names of the Tag Manager SDK classes this target links into the app, in the order it references them.
6
+ FOUNDATION_EXPORT NSArray<NSString *> *GoogleTagManagerLinkedClassNames(void);
7
+
8
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,35 @@
1
+ import Foundation
2
+ import Capacitor
3
+ #if canImport(GoogleTagManagerLinkage)
4
+ import GoogleTagManagerLinkage
5
+ #endif
6
+
7
+ /// Whether the Tag Manager SDK's classes are in the running app.
8
+ enum TagManagerSdk {
9
+ /// The classes the plugin links in. Calling this is also what keeps the linkage in the binary.
10
+ static var linkedClassNames: [String] {
11
+ GoogleTagManagerLinkedClassNames()
12
+ }
13
+
14
+ /// Asks the Objective-C runtime, by name, for every class the plugin links in, so the answer reflects the binary
15
+ /// that is running rather than what the plugin was compiled against.
16
+ static func isLinked(classNames: [String] = linkedClassNames) -> Bool {
17
+ !classNames.isEmpty && classNames.allSatisfy { NSClassFromString($0) != nil }
18
+ }
19
+ }
20
+
21
+ /// Google Tag Manager on iOS. The plugin exists to link the SDK into the app: the SDK finds the container in the app's
22
+ /// `container` folder and the Firebase Analytics events on its own, and handles the `tagmanager.c.<bundle id>` preview
23
+ /// links the app's Info.plist registers. What is left for the web view is the one question of whether the SDK is there.
24
+ @objc(GoogleTagManagerPlugin)
25
+ public class GoogleTagManagerPlugin: CAPPlugin, CAPBridgedPlugin {
26
+ public let identifier = "GoogleTagManagerPlugin"
27
+ public let jsName = "GoogleTagManager"
28
+ public let pluginMethods: [CAPPluginMethod] = [
29
+ CAPPluginMethod(name: "isAvailable", returnType: CAPPluginReturnPromise)
30
+ ]
31
+
32
+ @objc func isAvailable(_ call: CAPPluginCall) {
33
+ call.resolve(["available": TagManagerSdk.isLinked()])
34
+ }
35
+ }
@@ -0,0 +1,33 @@
1
+ import XCTest
2
+ import Capacitor
3
+ @testable import GoogleTagManagerPlugin
4
+
5
+ /// The JavaScript reaches the plugin by `jsName` and each method by its name: a rename on either side fails only at run
6
+ /// time on a phone, so both are pinned here.
7
+ final class GoogleTagManagerPluginTests: XCTestCase {
8
+ func testIsRegisteredUnderTheNameTheJavaScriptUses() {
9
+ let plugin = GoogleTagManagerPlugin()
10
+ XCTAssertEqual(plugin.jsName, "GoogleTagManager")
11
+ XCTAssertEqual(plugin.identifier, "GoogleTagManagerPlugin")
12
+ }
13
+
14
+ func testRegistersExactlyTheMethodsOfTheApi() {
15
+ let names = GoogleTagManagerPlugin().pluginMethods.map(\.name).sorted()
16
+ XCTAssertEqual(names, ["isAvailable"])
17
+ for name in names {
18
+ XCTAssertTrue(GoogleTagManagerPlugin().responds(to: NSSelectorFromString("\(name):")), "no @objc \(name)(_:)")
19
+ }
20
+ }
21
+
22
+ /// The test bundle links the plugin the way an app does, so the SDK has to be found here: a dependency that is
23
+ /// declared but never reaches the binary fails this rather than a phone.
24
+ func testFindsTheSdkInTheBinary() {
25
+ XCTAssertTrue(TagManagerSdk.linkedClassNames.contains("TAGManager"))
26
+ XCTAssertTrue(TagManagerSdk.isLinked())
27
+ }
28
+
29
+ func testReportsAClassThatIsNotThereAsMissing() {
30
+ XCTAssertFalse(TagManagerSdk.isLinked(classNames: ["TAGManager", "TAGNoSuchClass"]))
31
+ XCTAssertFalse(TagManagerSdk.isLinked(classNames: []))
32
+ }
33
+ }
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "capacitor-google-tag-manager",
3
+ "version": "0.1.0",
4
+ "description": "Brings the Google Tag Manager SDK into a Capacitor app on Android and iOS, with its preview activity, so a container shipped with the app is loaded",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "android/src/main/",
11
+ "android/build.gradle",
12
+ "dist/",
13
+ "ios/Sources",
14
+ "ios/Tests",
15
+ "Package.swift",
16
+ "CapacitorGoogleTagManager.podspec"
17
+ ],
18
+ "author": "Maxim Belov",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/MaximBelov/capacitor-google-tag-manager.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/MaximBelov/capacitor-google-tag-manager/issues"
26
+ },
27
+ "keywords": [
28
+ "capacitor",
29
+ "plugin",
30
+ "native",
31
+ "google-tag-manager",
32
+ "gtm",
33
+ "tag-manager",
34
+ "firebase-analytics"
35
+ ],
36
+ "scripts": {
37
+ "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
38
+ "verify:ios": "xcodebuild build -scheme CapacitorGoogleTagManager -destination generic/platform=iOS -derivedDataPath build/ios-device",
39
+ "verify:android": "cd android && ./gradlew clean build test",
40
+ "verify:web": "npm run build",
41
+ "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
42
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
43
+ "eslint": "eslint",
44
+ "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
45
+ "swiftlint": "node-swiftlint",
46
+ "docgen": "docgen --api GoogleTagManagerPlugin --output-readme README.md --output-json dist/docs.json",
47
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
48
+ "clean": "rimraf ./dist",
49
+ "watch": "tsc --watch",
50
+ "prepublishOnly": "npm run build",
51
+ "test": "npm run test:unit",
52
+ "test:unit": "vitest run",
53
+ "test:unit:coverage": "vitest run --coverage",
54
+ "test:parity": "node --test test/unit/platform-parity.test.mjs",
55
+ "test:android": "cd android && ./gradlew test",
56
+ "test:ios": "./scripts/test-ios.sh",
57
+ "example:install": "cd example-app && npm install --no-audit --no-fund",
58
+ "example:android:build": "npm run example:install && cd example-app && npm run build && npx cap sync android && cd android && ./gradlew assembleDebug",
59
+ "example:ios:build": "npm run example:install && cd example-app && npm run build && npx cap sync ios && xcodebuild build -project ios/App/App.xcodeproj -scheme App -destination generic/platform=iOS -derivedDataPath ../build/example-ios CODE_SIGNING_ALLOWED=NO"
60
+ },
61
+ "devDependencies": {
62
+ "@capacitor/android": "^8.0.0",
63
+ "@capacitor/core": "^8.0.0",
64
+ "@capacitor/docgen": "^0.3.1",
65
+ "@capacitor/ios": "^8.0.0",
66
+ "@ionic/eslint-config": "^0.5.0",
67
+ "@ionic/prettier-config": "^4.0.0",
68
+ "@ionic/swiftlint-config": "^2.0.0",
69
+ "eslint": "^10.14.0",
70
+ "prettier": "^3.6.2",
71
+ "prettier-plugin-java": "^2.7.7",
72
+ "rimraf": "^6.1.0",
73
+ "rollup": "^4.53.2",
74
+ "swiftlint": "^2.0.0",
75
+ "typescript": "~6.0.3",
76
+ "@vitest/coverage-v8": "^5.1.0",
77
+ "vitest": "^5.1.0"
78
+ },
79
+ "peerDependencies": {
80
+ "@capacitor/core": ">=8.0.0"
81
+ },
82
+ "prettier": "@ionic/prettier-config",
83
+ "swiftlint": "@ionic/swiftlint-config",
84
+ "capacitor": {
85
+ "ios": {
86
+ "src": "ios"
87
+ },
88
+ "android": {
89
+ "src": "android"
90
+ }
91
+ },
92
+ "engines": {
93
+ "node": ">=20"
94
+ }
95
+ }