Skip to content

Commit

Permalink
[expo-sms] Adds Attachment Support To SMS
Browse files Browse the repository at this point in the history
  • Loading branch information
thorbenprimke committed Apr 24, 2020
1 parent d7ba626 commit 3103f14
Show file tree
Hide file tree
Showing 16 changed files with 288 additions and 24 deletions.
17 changes: 17 additions & 0 deletions apps/test-suite/tests/SMS.ios.js
Expand Up @@ -3,6 +3,11 @@ import * as SMS from 'expo-sms';

import { expectMethodToThrowAsync } from '../TestUtils';
import { isInteractive } from '../utils/Environment';
import {
testSMSComposeWithSingleImageAttachment,
testSMSComposeWithTwoImageAttachments,
testSMSComposeWithAudioAttachment,
} from './SMSCommon';

export const name = 'SMS';

Expand All @@ -14,6 +19,18 @@ export function test({ describe, it, expect }) {
// TODO: Bacon: Build an API to close the UI Controller
await SMS.sendSMSAsync(['0123456789', '9876543210'], 'test');
});

it(`opens an SMS composer with single image attachment`, async () => {
await testSMSComposeWithSingleImageAttachment(expect);
});

it(`opens an SMS composer with two image attachments`, async () => {
await testSMSComposeWithTwoImageAttachments(expect);
});

it(`opens an SMS composer with audio attachment`, async () => {
await testSMSComposeWithAudioAttachment(expect);
});
}
} else {
it(`is unavailable`, async () => {
Expand Down
17 changes: 17 additions & 0 deletions apps/test-suite/tests/SMS.js
Expand Up @@ -2,6 +2,11 @@ import * as SMS from 'expo-sms';
import { Platform } from 'react-native';

import { isInteractive } from '../utils/Environment';
import {
testSMSComposeWithSingleImageAttachment,
testSMSComposeWithAudioAttachment,
testSMSComposeWithTwoImageAttachments,
} from './SMSCommon';

export const name = 'SMS';

Expand All @@ -11,6 +16,18 @@ export function test({ describe, it, expect }) {
it(`opens an SMS composer`, async () => {
await SMS.sendSMSAsync(['0123456789', '9876543210'], 'test');
});

it(`opens an SMS composer with single image attachment`, async () => {
await testSMSComposeWithSingleImageAttachment(expect);
});

it(`opens an SMS composer with two image attachments. Only first one is used.`, async () => {
await testSMSComposeWithTwoImageAttachments(expect);
});

it(`opens an SMS composer with audio attachment`, async () => {
await testSMSComposeWithAudioAttachment(expect);
});
});
}

Expand Down
92 changes: 92 additions & 0 deletions apps/test-suite/tests/SMSCommon.js
@@ -0,0 +1,92 @@
import * as FS from 'expo-file-system';
import * as SMS from 'expo-sms';

async function assertExists(testFile, expectedToExist, expect) {
const { exists } = await FS.getInfoAsync(testFile.localUri);
if (expectedToExist) {
expect(exists).toBeTruthy();
} else {
expect(exists).not.toBeTruthy();
}
}

async function loadAndSaveFle(fileInfo, expect) {
await FS.deleteAsync(fileInfo.localUri, { idempotent: true });
await assertExists(fileInfo, false, expect);
const { md5, headers } = await FS.downloadAsync(fileInfo.remoteUri, fileInfo.localUri, {
md5: true,
});
expect(md5).toBe(fileInfo.md5);
await assertExists(fileInfo, true, expect);
expect(headers['Content-Type'] || headers['content-type']).toBe(fileInfo.mimeType);
}

async function cleanupFile(fileInfo, expect) {
await FS.deleteAsync(fileInfo.localUri);
await assertExists(fileInfo, false, expect);
}

const pngFile = {
localUri: FS.documentDirectory + 'image.png',
remoteUri: 'https://s3-us-west-1.amazonaws.com/test-suite-data/avatar2.png',
md5: '1e02045c10b8f1145edc7c8375998f87',
mimeType: 'image/png',
};

const gifFile = {
localUri: FS.documentDirectory + 'image.gif',
remoteUri: 'https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif',
md5: '090592ebd01ac1e425b2766989040f80',
mimeType: 'image/gif',
};

const audioFile = {
localUri: FS.documentDirectory + 'sound.mp3',
remoteUri: 'https://dl.espressif.com/dl/audio/gs-16b-1c-44100hz.mp3',
md5: 'e21d733c3506280974842f11c6d30005',
mimeType: 'audio/mpeg',
};

const numbers = ['0123456789', '9876543210'];

export async function testSMSComposeWithSingleImageAttachment(expect) {
await loadAndSaveFle(pngFile, expect);
const contentUri = await FS.getContentUriAsync(pngFile.localUri);
await SMS.sendSMSAsync(numbers, 'test with image', {
uri: contentUri,
mimeType: 'image/png',
filename: 'image.png',
});
await cleanupFile(pngFile, expect);
}

export async function testSMSComposeWithTwoImageAttachments(expect) {
await loadAndSaveFle(pngFile, expect);
await loadAndSaveFle(gifFile, expect);
await SMS.sendSMSAsync(numbers, 'test with two images', [
{
uri: await FS.getContentUriAsync(pngFile.localUri),
mimeType: 'image/png',
filename: 'image.png',
},
{
uri: await FS.getContentUriAsync(gifFile.localUri),
mimeType: 'image/gif',
filename: 'image.gif',
},
]);
await cleanupFile(pngFile, expect);
await cleanupFile(gifFile, expect);
}

export async function testSMSComposeWithAudioAttachment(expect) {
await loadAndSaveFle(audioFile, expect);
await SMS.sendSMSAsync(numbers, 'test with audio', [
{
uri: await FS.getContentUriAsync(audioFile.localUri),
mimeType: 'audio/mpeg',
filename: 'sound.mp3',
},
]);
await cleanupFile(audioFile, expect);
}
21 changes: 19 additions & 2 deletions docs/pages/versions/unversioned/sdk/sms.md
Expand Up @@ -47,7 +47,9 @@ Opens the default UI/app for sending SMS messages with prefilled addresses and m

- **addresses (_Array\<string\>|string_)** -- An array of addresses (_phone numbers_) or single address passed as strings. Those would appear as recipients of the prepared message.

- **message (_string_)** -- Message to be sent
- **message (_string_)** -- Message to be sent.

- **attachments (_optional_) (_Array<\object_\>|_object_)** -- An array of [SMSAttachment](#smsattachment) objects or single object. Android supports only one attachment.

#### Returns

Expand All @@ -66,6 +68,21 @@ Android does not provide information about the status of the SMS message, so on
```javascript
const { result } = await SMS.sendSMSAsync(
['0123456789', '9876543210'],
'My sample HelloWorld message'
'My sample HelloWorld message',
{
uri: 'path/myfile.png',
mimeType: 'image/png',
filename: 'myfile.png',
}
);
```

## Related types

### SMSAttachment

An object that is used to describe an attachment that is included with a SMS message.

- **uri (_string_)** -- the content URI of the attachment. The URI needs be a content URI so that it can be accessed by other applications outside of Expo. (See [FileSystem.getContentUriAsync](../filesystem/#filesystemgetcontenturiasyncfileuri))
- **mimeType (_string_)** -- the mime type of the attachment such as `image/png`
- **filename (_string_)** -- the filename of the attachment
2 changes: 2 additions & 0 deletions packages/expo-sms/CHANGELOG.md
Expand Up @@ -6,4 +6,6 @@

### 🎉 New features

- Add `attachments` as an optional parameter to `sendSMSAsync`. It can be used to provide an attachment along with the recipients and message arguments. ([#7967](https://github.com/expo/expo/pull/7967) by [@thorbenprimke](https://github.com/thorbenprimke))

### 🐛 Bug fixes
1 change: 1 addition & 0 deletions packages/expo-sms/android/build.gradle
Expand Up @@ -59,4 +59,5 @@ if (new File(rootProject.projectDir.parentFile, 'package.json').exists()) {
dependencies {
unimodule "unimodules-core"
unimodule "unimodules-permissions-interface"
implementation 'androidx.annotation:annotation:1.1.0'
}
Expand Up @@ -5,9 +5,11 @@
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Telephony;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.unimodules.core.ExportedModule;
import org.unimodules.core.ModuleRegistry;
Expand All @@ -17,6 +19,8 @@
import org.unimodules.core.interfaces.LifecycleEventListener;
import org.unimodules.core.interfaces.services.UIManager;

import androidx.annotation.Nullable;

public class SMSModule extends ExportedModule implements LifecycleEventListener {
private static final String TAG = "ExpoSMS";
private static final String ERROR_TAG = "E_SMS";
Expand Down Expand Up @@ -53,23 +57,37 @@ public void onDestroy() {
}

@ExpoMethod
public void sendSMSAsync(final ArrayList<String> addresses, final String message, final Promise promise) {
public void sendSMSAsync(
final ArrayList<String> addresses,
final String message,
final @Nullable List<Map<String, String>> attachments,
final Promise promise) {
if (mPendingPromise != null) {
promise.reject(ERROR_TAG + "_SENDING_IN_PROGRESS", "Different SMS sending in progress. Await the old request and then try again.");
return;
}

final Intent SMSIntent = new Intent(Intent.ACTION_SENDTO);
Intent SMSIntent = new Intent(Intent.ACTION_SEND);
String defaultSMSPackage = Telephony.Sms.getDefaultSmsPackage(getContext());
if (defaultSMSPackage != null){
SMSIntent.setPackage(defaultSMSPackage);
} else {
promise.reject(ERROR_TAG + "_NO_SMS_APP", "No messaging application available");
return;
}
SMSIntent.setType("text/plain");
final String smsTo = constructRecipients(addresses);
SMSIntent.setData(Uri.parse("smsto:" + smsTo));
SMSIntent.putExtra("address", smsTo);

SMSIntent.putExtra("exit_on_sent", true);
SMSIntent.putExtra("compose_mode", true);
SMSIntent.putExtra(Intent.EXTRA_TEXT, message);
SMSIntent.putExtra("sms_body", message);

if (SMSIntent.resolveActivity(getContext().getPackageManager()) == null) {
promise.reject(ERROR_TAG + "_NO_SMS_APP", "No messaging application available");
return;
if (attachments != null && !attachments.isEmpty()) {
Map<String, String> attachment = attachments.get(0);
SMSIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(attachment.get("uri")));
SMSIntent.setType(attachment.get("mimeType"));
}

mPendingPromise = promise;
Expand Down
4 changes: 2 additions & 2 deletions packages/expo-sms/build/SMS.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 17 additions & 4 deletions packages/expo-sms/build/SMS.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/expo-sms/build/SMS.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions packages/expo-sms/build/SMS.types.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/expo-sms/build/SMS.types.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions packages/expo-sms/ios/EXSMS/EXSMSModule.m
Expand Up @@ -3,6 +3,11 @@
#import <MessageUI/MessageUI.h>
#import <EXSMS/EXSMSModule.h>
#import <UMCore/UMUtilities.h>
#if SD_MAC
#import <CoreServices/CoreServices.h>
#else
#import <MobileCoreServices/MobileCoreServices.h>
#endif

@interface EXSMSModule () <MFMessageComposeViewControllerDelegate>

Expand Down Expand Up @@ -31,6 +36,7 @@ - (void)setModuleRegistry:(UMModuleRegistry *)moduleRegistry
UM_EXPORT_METHOD_AS(sendSMSAsync,
sendSMS:(NSArray<NSString *> *)addresses
message:(NSString *)message
attachments:(NSArray<NSDictionary *>*)attachments
resolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
Expand All @@ -51,6 +57,32 @@ - (void)setModuleRegistry:(UMModuleRegistry *)moduleRegistry
messageComposeViewController.messageComposeDelegate = self;
messageComposeViewController.recipients = addresses;
messageComposeViewController.body = message;

if (attachments) {
for (NSDictionary* attachment in attachments) {
NSString *mimeType = attachment[@"mimeType"];
CFStringRef mimeTypeRef = (__bridge CFStringRef)mimeType;
CFStringRef utiRef = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeTypeRef, NULL);
if (utiRef == NULL) {
reject(@"E_SMS_ATTACHMENT", [NSString stringWithFormat:@"Failed to find UTI for mimeType: %@", mimeType], nil);
_resolve = nil;
_reject = nil;
return;
}
NSString *typeIdentifier = (__bridge_transfer NSString *)utiRef;
NSString *uri = attachment[@"uri"];
NSString *filename = attachment[@"filename"];
NSError *error;
NSData *attachmentData = [NSData dataWithContentsOfURL:[NSURL URLWithString:uri] options:(NSDataReadingOptions)0 error:&error];
bool attached = [messageComposeViewController addAttachmentData:attachmentData typeIdentifier:typeIdentifier filename:filename];
if (!attached) {
reject(@"E_SMS_ATTACHMENT", [NSString stringWithFormat:@"Failed to attach file: %@", uri], nil);
_resolve = nil;
_reject = nil;
return;
}
}
}

UM_WEAKIFY(self);
[UMUtilities performSynchronouslyOnMainThread:^{
Expand Down

0 comments on commit 3103f14

Please sign in to comment.