diff --git a/.github/workflows/create_test_patches.yml b/.github/workflows/create_test_patches.yml index ee0df214..61225012 100644 --- a/.github/workflows/create_test_patches.yml +++ b/.github/workflows/create_test_patches.yml @@ -38,7 +38,7 @@ jobs: id: yarn-cache with: path: .yarn/cache - key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'RNGoogleMobileAdsExample/package.json') }} + key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'yarn.lock', 'packages/core/package.json', 'RNGoogleMobileAdsExample/package.json') }} restore-keys: | ${{ runner.os }}-yarn- - name: Yarn Install @@ -54,13 +54,11 @@ jobs: # yarn3+ by default disables lockfile alteration in CI. We want it. YARN_ENABLE_IMMUTABLE_INSTALLS: false run: | - export PACKAGE_NAME=`npx json name -f package.json` + export PACKAGE_NAME=`npx json name -f packages/core/package.json` export CLEAN_PACKAGE_NAME=`echo $PACKAGE_NAME | sed s/@// |sed s,/,-,` mkdir $HOME/scratch echo "Packing PR version of $CLEAN_PACKAGE_NAME" - yarn pack - pwd - mv package.tgz $HOME/scratch/$CLEAN_PACKAGE_NAME.tgz + yarn workspace react-native-google-mobile-ads pack --out $HOME/scratch/$CLEAN_PACKAGE_NAME.tgz ls -la $HOME/scratch/*$CLEAN_PACKAGE_NAME* cd $HOME npx @react-native-community/cli init template --skip-install --skip-git-init @@ -76,7 +74,7 @@ jobs: \rm -fr react-native-google-mobile-ads mv package react-native-google-mobile-ads # do some cleaning so the patch is minimal - TODO fix .npmignore/package.json files block - rm react-native-google-mobile-ads/CHANGELOG.md + rm -f react-native-google-mobile-ads/CHANGELOG.md popd npx patch-package $PACKAGE_NAME || true ls -la $HOME/template/patches @@ -93,7 +91,7 @@ jobs: if: "${{ github.ref == 'refs/heads/main' }}" with: path: .yarn/cache - key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'RNGoogleMobileAdsExample/package.json') }} + key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'yarn.lock', 'packages/core/package.json', 'RNGoogleMobileAdsExample/package.json') }} # create a comment on the PR and any related issues with a direct link to the archive, # a call for testers, and perhaps a paste-able set of commands to install them diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 39e209f7..c9f6aa6d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,7 +25,7 @@ jobs: id: yarn-cache with: path: .yarn/cache - key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'RNGoogleMobileAdsExample/package.json') }} + key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'yarn.lock', 'packages/core/package.json', 'RNGoogleMobileAdsExample/package.json') }} restore-keys: | ${{ runner.os }}-yarn- - name: Yarn Install diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 1c44cd01..675309e0 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -29,7 +29,7 @@ jobs: id: yarn-cache with: path: .yarn/cache - key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'RNGoogleMobileAdsExample/package.json') }} + key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'yarn.lock', 'packages/core/package.json', 'RNGoogleMobileAdsExample/package.json') }} restore-keys: | ${{ runner.os }}-yarn- - name: Configure JDK @@ -46,6 +46,8 @@ jobs: command: yarn - name: Lint run: yarn lint:code + - name: Kotlin lint + run: ./gradlew ktlintCheck - name: Save Code Linting Report JSON run: yarn lint:report continue-on-error: true @@ -77,7 +79,7 @@ jobs: id: yarn-cache with: path: .yarn/cache - key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'RNGoogleMobileAdsExample/package.json') }} + key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'yarn.lock', 'packages/core/package.json', 'RNGoogleMobileAdsExample/package.json') }} restore-keys: | ${{ runner.os }}-yarn- - name: Yarn Install diff --git a/.github/workflows/tests_e2e_android.yml b/.github/workflows/tests_e2e_android.yml index 391875c4..a7e71b2d 100644 --- a/.github/workflows/tests_e2e_android.yml +++ b/.github/workflows/tests_e2e_android.yml @@ -88,7 +88,7 @@ jobs: id: yarn-cache with: path: .yarn/cache - key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'RNGoogleMobileAdsExample/package.json') }} + key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'yarn.lock', 'packages/core/package.json', 'RNGoogleMobileAdsExample/package.json') }} restore-keys: ${{ runner.os }}-yarn-v1 - name: Yarn Install @@ -97,7 +97,7 @@ jobs: timeout_minutes: 10 retry_wait_seconds: 60 max_attempts: 3 - command: yarn && cd RNGoogleMobileAdsExample && DETOX_DISABLE_POSTINSTALL=1 yarn + command: yarn - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 @@ -205,7 +205,9 @@ jobs: disable-animations: true script: | $ANDROID_HOME/platform-tools/adb logcat '*:D' > adb-log.txt & - yarn tests:android:run + yarn tests:appium:drivers:install + yarn tests:appium:drivers:verify + yarn tests:appium:android - uses: codecov/codecov-action@v5 with: diff --git a/.github/workflows/tests_e2e_ios.yml b/.github/workflows/tests_e2e_ios.yml index 9a10c0b4..e7855c29 100644 --- a/.github/workflows/tests_e2e_ios.yml +++ b/.github/workflows/tests_e2e_ios.yml @@ -57,10 +57,10 @@ jobs: id: yarn-cache with: path: .yarn/cache - key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'RNGoogleMobileAdsExample/package.json') }} + key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'yarn.lock', 'packages/core/package.json', 'RNGoogleMobileAdsExample/package.json') }} restore-keys: ${{ runner.os }}-yarn-v1 - # Detox is compiled during yarn install, using Xcode, set up cache first + # Native iOS deps compile during yarn/pod install; set up Xcode compile cache first - uses: hendrikmuhs/ccache-action@v1 name: Xcode Compile Cache with: @@ -95,7 +95,7 @@ jobs: timeout_minutes: 10 retry_wait_seconds: 60 max_attempts: 3 - command: yarn && yarn tests:install + command: yarn - name: Pod Install uses: nick-fields/retry@v3 @@ -105,14 +105,6 @@ jobs: max_attempts: 3 command: yarn tests:ios:pod:install - - name: Install applesimutils - uses: nick-fields/retry@v3 - with: - timeout_minutes: 5 - retry_wait_seconds: 60 - max_attempts: 3 - command: HOMEBREW_NO_AUTO_UPDATE=1 brew tap wix/brew && HOMEBREW_NO_AUTO_UPDATE=1 brew install applesimutils - - name: Metro Bundler Cache uses: actions/cache@v4 with: @@ -133,16 +125,22 @@ jobs: echo "...javascript bundle ready" - name: Create Simulator Log - # With a little delay so the detox test below has time to spawn it, missing the first part of boot is fine - # If you boot the simulator separately from detox, some other race fails and detox testee never sends ready to proxy continue-on-error: true run: nohup sh -c "sleep 30 && xcrun simctl spawn booted log stream --level debug --style compact > simulator.log 2>&1 &" - - name: Build and Run e2e app + - name: Install example on simulator timeout-minutes: 20 continue-on-error: true run: yarn tests:ios:run + - name: Build and Run Appium e2e + timeout-minutes: 30 + continue-on-error: true + run: | + yarn tests:appium:drivers:install + yarn tests:appium:drivers:verify + yarn tests:appium:ios + - name: Upload Simulator Log uses: actions/upload-artifact@v4 if: always() diff --git a/.github/workflows/tests_jest.yml b/.github/workflows/tests_jest.yml index 090d951e..525c2057 100644 --- a/.github/workflows/tests_jest.yml +++ b/.github/workflows/tests_jest.yml @@ -41,7 +41,7 @@ jobs: id: yarn-cache with: path: .yarn/cache - key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'RNGoogleMobileAdsExample/package.json') }} + key: ${{ runner.os }}-yarn-${{ hashFiles('package.json', 'yarn.lock', 'packages/core/package.json', 'RNGoogleMobileAdsExample/package.json') }} restore-keys: | ${{ runner.os }}-yarn- - name: Yarn Install diff --git a/.gitignore b/.gitignore index d17c2154..81f9d438 100644 --- a/.gitignore +++ b/.gitignore @@ -541,7 +541,6 @@ Pods/* .idea/**/* **/Pods/** **/dist/ -lib/version.js typedoc.raw.json tests/ios/Firebase tests/ios/resetXcode.sh @@ -551,7 +550,7 @@ google-services.json GoogleService-Info.plist # generated files -src/version.ts +packages/core/src/version.ts RNFBVersion.m ReactNativeFirebaseVersion.java @@ -587,3 +586,9 @@ lib/ # Generated by react-native at times RNGoogleMobileAdsExample/ios/.xcode.env.local + +# Nx local cache +.nx/ + +# Appium driver install tree (pins live in tooling/appium/drivers.manifest.json) +tooling/appium/.appium-home/ diff --git a/.prettierignore b/.prettierignore index c59a25d0..dd8f9ec6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,5 @@ package.json **/node_modules/** +packages/**/lib/** +packages/**/plugin/build/** +.nx/ diff --git a/.releaserc b/.releaserc index afb731f2..0aee25fd 100644 --- a/.releaserc +++ b/.releaserc @@ -11,12 +11,15 @@ "preset": "conventionalcommits" }], "@semantic-release/changelog", - "@semantic-release/npm", + ["@semantic-release/npm", { + "pkgRoot": "packages/core" + }], [ "@semantic-release/git", { "assets": [ - "package.json", + "packages/core/package.json", + "lerna.json", "CHANGELOG.md" ], "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" diff --git a/.spellcheck.dict.txt b/.spellcheck.dict.txt index 1f1daaee..0bc6d44b 100644 --- a/.spellcheck.dict.txt +++ b/.spellcheck.dict.txt @@ -151,3 +151,40 @@ SSV CP-User Intellisense onAdEvent +AdChoices +AdMob's +AdsConsent +AdsConsentStatus +AppLovin +ApplicationId +ATT +backfill +customizations +DNS +EAS +explainer +groupId +IAB +IDFA +isClicked +isClosed +isEarnedReward +isLoaded +isOpened +isShowing +logcat +NativeAd +NativeAdEventType +NativeAdView +NativeAsset +NativeMediaView +PaidEvent +preload +Programmatically +RequestOptions +RewardedAd +RewardedAdReward +SKAdNetwork +testDeviceIdentifiers +unmuted +v15 diff --git a/AGENTS.md b/AGENTS.md index c6dc3727..81780239 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,8 @@ # React Native Google Mobile Ads — agents -`src/` → `lib/` via `yarn prepare`. Native `android/` `ios/`. Expo `plugin/`. Example `RNGoogleMobileAdsExample/`. E2e `e2e/`. Docs `docs/`. Contributor/review norms: `CONTRIBUTING.md`. +Yarn workspaces monorepo. Publishable package: `packages/core/` (`react-native-google-mobile-ads`). `packages/core/src/` → `lib/` + Expo plugin via `yarn prepare` (runs `yarn lerna:prepare` / Lerna+Nx). Shared package TS: `tsconfig.packages.base.json` (packages extend it). Shared ESLint: root `eslint.config.js`. Native `packages/core/android/` `packages/core/ios/`. Expo `packages/core/plugin/`. Example workspace `RNGoogleMobileAdsExample/`. Appium workspace `tooling/appium/` (`@invertase/rngma-appium`; [Appium](okf-bundle/testing/running-e2e.md#appium-scaffold)). Docs `docs/`. Contributor/review norms: `CONTRIBUTING.md`. -Root `yarn`. Shell: [agent-command-policy](okf-bundle/testing/agent-command-policy.md) only. E2e: [running-e2e § agent rule](okf-bundle/testing/running-e2e.md#agent-rule-read-first) and [platform coverage](okf-bundle/testing/running-e2e.md#platform-coverage-gate-blocking). A green e2e workflow is **not** a pass ([continue-on-error](okf-bundle/ci-workflows/index.md#e2e-continue-on-error)); use local counts + `/tmp/rngma-e2e-*.log`. Loop: [change-authoring](okf-bundle/testing/change-authoring-workflow.md#loop) (`documentation?` then frozen [OKF scan](okf-bundle/testing/validation-checklist.md#okf-bundle-review) when `okf-bundle/`, `AGENTS.md`, or `CONTRIBUTING.md` changed; freeze: [§ frozen tree](okf-bundle/testing/change-authoring-workflow.md#frozen-tree)). Before commit or push: [validation evidence package](okf-bundle/testing/validation-checklist.md#validation-evidence-package). [Coverage evidence](okf-bundle/testing/coverage-design.md#coverage-evidence-package) only when `src/` **or** `android/` **or** `ios/` **or** `plugin/` TS is in the diff; `app.plugin.js`-only is `n/a` unless `plugin/` TS changed ([§ gates](okf-bundle/testing/change-authoring-workflow.md#gates)). Index: [okf-bundle](okf-bundle/index.md). Testing: [testing/index.md](okf-bundle/testing/index.md). Match work type and validation tier: [iteration vocabulary](okf-bundle/testing/iteration-vocabulary.md). +Root `yarn`. Root `./gradlew ktlintFormat` formats `packages/core` Android Kotlin (optional `-PinternalKtlintGitFilter`); check-only: `./gradlew ktlintCheck`. Optional local hook: `./gradlew addKtlintFormatGitPreCommitHook`; Invertase global pre-commit also invokes root `./gradlew ktlintFormat` when present (this repo does not ship an installed hook). Java remains `yarn lint:android`. Example Android builds stay under `RNGoogleMobileAdsExample/android/`. Native JVM/XCTest unit: `yarn tests:android:unit` / `yarn tests:ios:unit` ([agent command policy](okf-bundle/testing/agent-command-policy.md)). Shell: [agent-command-policy](okf-bundle/testing/agent-command-policy.md) only. E2e: [running-e2e § agent rule](okf-bundle/testing/running-e2e.md#agent-rule-read-first) and [platform coverage](okf-bundle/testing/running-e2e.md#platform-coverage-gate-blocking). A green e2e workflow is **not** a pass ([continue-on-error](okf-bundle/ci-workflows/index.md#e2e-continue-on-error)); use local counts + `/tmp/rngma-e2e-*.log`. Loop: [change-authoring](okf-bundle/testing/change-authoring-workflow.md#loop) (`documentation?` then frozen [OKF scan](okf-bundle/testing/validation-checklist.md#okf-bundle-review) when `okf-bundle/`, `AGENTS.md`, or `CONTRIBUTING.md` changed; freeze: [§ frozen tree](okf-bundle/testing/change-authoring-workflow.md#frozen-tree)). Before commit or push: [validation evidence package](okf-bundle/testing/validation-checklist.md#validation-evidence-package). [Coverage evidence](okf-bundle/testing/coverage-design.md#coverage-evidence-package) only when `packages/core/src/` **or** `packages/core/android/` **or** `packages/core/ios/` **or** `packages/core/plugin/` TS is in the diff; `packages/core/app.plugin.js`-only is `n/a` unless plugin TS changed ([§ gates](okf-bundle/testing/change-authoring-workflow.md#gates)). Index: [okf-bundle](okf-bundle/index.md). Testing: [testing/index.md](okf-bundle/testing/index.md). Match work type and validation tier: [iteration vocabulary](okf-bundle/testing/iteration-vocabulary.md). Follow [documentation-policy § public vs ephemeral vs private](okf-bundle/documentation-policy.md#durable-vs-ephemeral). Do not stage `.agents/work-queues/` or `.agents/reports/`. Do not add queue files under `okf-bundle/`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 78703497..cc34f1db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,12 +70,19 @@ git clone https://github.com/invertase/react-native-google-mobile-ads.git cd react-native-google-mobile-ads ``` -### Step 2: Install test project dependencies +### Step 2: Install dependencies ```bash yarn +yarn prepare ``` +Root `yarn` installs all workspaces (library under `packages/core/`, `RNGoogleMobileAdsExample/`, and `tooling/*`). `yarn prepare` builds the library via Lerna/Nx (`yarn lerna:prepare`). Package TypeScript extends root `tsconfig.packages.base.json`; ESLint is the root `eslint.config.js`. + +Appium 3 + WebdriverIO lives in `tooling/appium/` (`@invertase/rngma-appium`). Pin contract: `yarn.lock` plus `tooling/appium/drivers.manifest.json`. Device-free check: `yarn tests:appium:validate`. Drivers: `yarn tests:appium:drivers:install` then `yarn tests:appium:drivers:verify`. Device smoke: `yarn tests:appium:android` / `yarn tests:appium:ios` after build/install prerequisites. Details: [Appium](okf-bundle/testing/running-e2e.md#appium-scaffold). + +Android **Java** format: `yarn lint:android`. Android **Kotlin** format: repo-root `./gradlew ktlintFormat` (check-only: `./gradlew ktlintCheck`). Optional: `./gradlew addKtlintFormatGitPreCommitHook` to install a local hook; Invertase global pre-commit also invokes root `./gradlew ktlintFormat` when present (this repo does not ship an installed hook). Example app builds remain under `RNGoogleMobileAdsExample/android/`. Native unit skeletons: `yarn tests:android:unit` (Robolectric) and `yarn tests:ios:unit` (XCTest) — see [agent command policy](okf-bundle/testing/agent-command-policy.md). + ## Submitting code for review All code changes should be submitted as a pull request to the `main` branch. diff --git a/RNGoogleMobileAdsExample/App.tsx b/RNGoogleMobileAdsExample/App.tsx index abaf75f2..b64dc5ba 100644 --- a/RNGoogleMobileAdsExample/App.tsx +++ b/RNGoogleMobileAdsExample/App.tsx @@ -1,5 +1,5 @@ /** - * Sample React Native App + * Sample React Native App — format gallery for manual QA and Appium smoke. * https://github.com/facebook/react-native * * @format @@ -8,7 +8,7 @@ /* eslint-disable no-console, @typescript-eslint/no-explicit-any */ import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'; -import React, { RefObject, useEffect, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Button, Image, @@ -20,8 +20,8 @@ import { View, useColorScheme, } from 'react-native'; -import { AutoExecutableTest, TestRegistry, TestResult, TestRunner, TestType } from 'jet'; +import { AppiumTestIds } from './src/appiumTestIds'; import MobileAds, { AdEventType, AdsConsent, @@ -34,6 +34,7 @@ import MobileAds, { GAMBannerAdSize, GAMInterstitialAd, InterstitialAd, + type MobileAd, NativeAd, NativeAdEventType, NativeAdView, @@ -52,7 +53,46 @@ import MobileAds, { useRewardedAd, useRewardedInterstitialAd, } from 'react-native-google-mobile-ads'; -import { MobileAd } from '../lib/typescript/ads/MobileAd'; + +type GallerySection = 'all' | 'formats' | 'hooks' | 'debug'; + +type GalleryEntry = { + id: string; + title: string; + section: Exclude; + render: () => React.ReactNode; +}; + +const GALLERY_SECTION_CHIPS: Array<{ id: GallerySection; title: string }> = [ + { id: 'all', title: 'All' }, + { id: 'formats', title: 'Formats' }, + { id: 'hooks', title: 'Hooks' }, + { id: 'debug', title: 'Debug' }, +]; + +function bannerVariantKey( + bannerAdSize: BannerAdSize | string, + maxHeight?: number, + width?: number, +): string { + return bannerAdSize + .split('_') + .map(s => s.toLowerCase().charAt(0).toUpperCase() + s.toLowerCase().slice(1)) + .join('') + .concat(maxHeight ? `MaxHeight${maxHeight}` : '') + .concat(width ? `Width${width}` : ''); +} + +function gamSizesKey(sizes: (keyof typeof GAMBannerAdSize)[]): string { + return sizes + .map(size => + size + .split('_') + .map((s: string) => s.toLowerCase().charAt(0).toUpperCase() + s.toLowerCase().slice(1)) + .join(''), + ) + .join('_'); +} function App() { const isDarkMode = useColorScheme() === 'dark'; @@ -67,9 +107,20 @@ function App() { function AppContent() { const safeAreaInsets = useSafeAreaInsets(); + const [selectedId, setSelectedId] = useState(null); + const [bannerMenuOpen, setBannerMenuOpen] = useState(false); + const [section, setSection] = useState('all'); + const entries = useMemo(() => buildGalleryEntries(), []); + + const bannerEntries = entries.filter(e => e.id.startsWith('gma.format.banner.')); + const primaryEntries = entries.filter(e => !e.id.startsWith('gma.format.banner.')); + const showBanners = section === 'all' || section === 'formats'; + const visiblePrimary = + section === 'all' ? primaryEntries : primaryEntries.filter(e => e.section === section); + const selected = entries.find(e => e.id === selectedId) ?? null; return ( - + - + {selected ? ( + + + {selected.title} +