Skip to content

Snapchat SnapKit config plugin #153

Description

@ansh

Library

https://www.npmjs.com/package/@snapchat/snap-kit-react-native

Summary

A Snapchat SnapKit config plugin would be great! Would allow us to use Snapchat's React Native package and it's forgo its complex setup process. (https://docs.snap.com/snap-kit/creative-kit/Tutorials/react-native#step-2-update-your-androidmanifestxml-file)

I already managed to get a config plugin sort of working. Can't figure out how to do step 3 (Step 3: Define Paths in Your res/xml/filre_paths.xml File) within the config plugin

Any existing examples?

import {
  AndroidConfig,
  createRunOncePlugin,
  IOSConfig,
  withXcodeProject,
  withAppBuildGradle,
  ConfigPlugin,
  withStringsXml,
  withProjectBuildGradle,
  withAndroidManifest,
} from "@expo/config-plugins";
import {
  createGeneratedHeaderComment,
  MergeResults,
  mergeContents,
  removeGeneratedContents,
} from "@expo/config-plugins/build/utils/generateCode";
import { ExpoConfig } from "expo/config";
const { addMetaDataItemToMainApplication, getMainApplicationOrThrow } = AndroidConfig.Manifest;

// Fork of config-plugins mergeContents, but appends the contents to the end of the file. Taken from https://github.com/expo/expo/blob/master/packages/expo-camera/plugin/src/withCamera.ts
function appendContents({
  src,
  newSrc,
  tag,
  comment,
}: {
  src: string;
  newSrc: string;
  tag: string;
  comment: string;
}): MergeResults {
  const header = createGeneratedHeaderComment(newSrc, tag, comment);
  if (!src.includes(header)) {
    // Ensure the old generated contents are removed.
    const sanitizedTarget = removeGeneratedContents(src, tag);
    const contentsToAdd = [
      // @something
      header,
      // contents
      newSrc,
      // @end
      `${comment} @generated end ${tag}`,
    ].join("\n");

    return {
      contents: sanitizedTarget ?? src + contentsToAdd,
      didMerge: true,
      didClear: !!sanitizedTarget,
    };
  }
  return { contents: src, didClear: false, didMerge: false };
}

const pkg = require("@snapchat/snap-kit-react-native/package.json");

const addSnapkitImport = (src: string): MergeResults => {
  return appendContents({
    tag: "expo-snapkit-import",
    src,
    newSrc: `allprojects { repositories { maven { url "https://storage.googleapis.com/snap-kit-build/maven" } } }`,
    comment: "//",
  });
};
const withSnapkitGradle: ConfigPlugin = (config) => {
  return withProjectBuildGradle(config, (config) => {
    if (config.modResults.language === "groovy") {
      config.modResults.contents = addSnapkitImport(config.modResults.contents).contents;
    } else {
      throw new Error(
        "Cannot add Snapkit maven gradle because the project build.gradle is not groovy"
      );
    }
    return config;
  });
};

async function addMetaDataToAndroidManifest(
  config: Pick<ExpoConfig, "android">,
  androidManifest: AndroidConfig.Manifest.AndroidManifest
): Promise<AndroidConfig.Manifest.AndroidManifest> {
  const name = "com.snapchat.kit.sdk.clientId";
  const value = "{{YOUR API KEY HERE}}";
  const mainApplication = getMainApplicationOrThrow(androidManifest); // Get the <application /> tag and assert if it doesn't exist.
  addMetaDataItemToMainApplication(
    mainApplication,
    name, // value for `android:name`
    value // value for `android:value`
  );
  return androidManifest;
}
const withCustomMetaData: ConfigPlugin = (config) => {
  return withAndroidManifest(config, async (config) => {
    // Modifiers can be async, but try to keep them fast.
    config.modResults = await addMetaDataToAndroidManifest(config, config.modResults);
    return config;
  });
};

function addProviderToAndroidManifest(androidManifest: AndroidConfig.Manifest.AndroidManifest) {
  const app = AndroidConfig.Manifest.getMainApplicationOrThrow(
    androidManifest
  ) as AndroidConfig.Manifest.ManifestApplication & { provider?: any[] };
  // Add the provider if it doesn't exist.
  if (!app.provider) {
    app.provider = [];
  }
  // if the provider doesn't have the FileProvider, add it.
  if (
    !app.provider.some((p) => p.$["android:name"] === "android.support.v4.content.FileProvider")
  ) {
    // <provider android:authorities="${applicationId}.fileprovider" android:name="android.support.v4.content.FileProvider" android:exported="false" android:grantUriPermissions="true"><meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"/></provider>
    app.provider.push({
      $: {
        "android:name": "android.support.v4.content.FileProvider",
        "android:authorities": "${applicationId}.fileprovider",
        "android:exported": "false",
        "android:grantUriPermissions": "true",
      },
      "meta-data": {
        $: {
          "android:name": "android.support.FILE_PROVIDER_PATHS",
          "android:resource": "@xml/file_paths",
        },
      },
    });
  }
  return androidManifest;
}
function withProvider(config: ExpoConfig) {
  return withAndroidManifest(config, async (config) => {
    config.modResults = addProviderToAndroidManifest(config.modResults);
    return config;
  });
}

function addPackageToQuery(androidManifest: AndroidConfig.Manifest.AndroidManifest) {
  // <package android:name="com.snapchat.android" />
  const packageToAdd = {
    package: {
      $: {
        "android:name": "com.snapchat.android",
      },
    },
  };
  // @ts-ignore since queries does exist but Expo's types don't have it.
  const queries = androidManifest.manifest.queries;
  if (!queries) {
    // @ts-ignore since queries does exist but Expo's types don't have it.
    androidManifest.manifest.queries = [...packageToAdd];
  } else {
    // @ts-ignore since queries does exist but Expo's types don't have it.
    // TODO: this will break if there are other <package> tags in the queries.
    androidManifest.manifest.queries[0].package = {
      $: { ...packageToAdd.package.$ },
    };
  }

  return androidManifest;
}
function withPackage(config: ExpoConfig) {
  return withAndroidManifest(config, async (config) => {
    config.modResults = addPackageToQuery(config.modResults);
    return config;
  });
}

const withSnapchatSdk: ConfigPlugin = (config) => {
  return withPackage((withCustomMetaData(withSnapkitGradle(config))));
  // skipping withProvider as filePath was giving me issues.
  // return withPackage(withProvider(withCustomMetaData(withSnapkitGradle(config))));
};

export default createRunOncePlugin(withSnapchatSdk, pkg.name, pkg.version);

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions